Skip to content

Commit

Permalink
[Emby] Early attempt at "Leaving Soon" collection
Browse files Browse the repository at this point in the history
  • Loading branch information
Schaka committed Mar 6, 2024
1 parent 3a685bc commit 22e1b82
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 50 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,63 @@
package com.github.schaka.janitorr.mediaserver

import com.github.schaka.janitorr.mediaserver.jellyfin.JellyfinRestService
import com.github.schaka.janitorr.mediaserver.jellyfin.filesystem.PathStructure
import com.github.schaka.janitorr.mediaserver.jellyfin.library.LibraryType
import com.github.schaka.janitorr.servarr.LibraryItem
import org.slf4j.LoggerFactory
import java.nio.file.Files
import java.nio.file.Path

interface MediaServerService {
fun cleanupTvShows(items: List<LibraryItem>)
abstract class MediaServerService {

fun cleanupMovies(items: List<LibraryItem>)
companion object {
private val log = LoggerFactory.getLogger(this::class.java.enclosingClass)
@JvmStatic
protected val seasonPattern = Regex("Season (?<season>\\d+)")
private val filePattern = Regex("^.*\\.(mkv|mp4|avi|webm|mts|m2ts|ts|wmv|mpg|mpeg|mp2|m2v|m4v)\$")
private val numberPattern = Regex("[0-9]+")
}

fun updateGoneSoon(type: LibraryType, items: List<LibraryItem>, onlyAddLinks: Boolean = false)
abstract fun cleanupTvShows(items: List<LibraryItem>)

abstract fun cleanupMovies(items: List<LibraryItem>)

abstract fun updateGoneSoon(type: LibraryType, items: List<LibraryItem>, onlyAddLinks: Boolean = false)

protected fun isMediaFile(path: String) =
filePattern.matches(path)

fun parseMetadataId(value: String?): Int? {
return value?.let {
numberPattern.findAll(it)
.map(MatchResult::value)
.map(String::toInt)
.firstOrNull()
}
}

protected fun createSymLink(source: Path, target: Path, type: String) {
if (!Files.exists(target)) {
log.debug("Creating {} link from {} to {}", type, source, target)
Files.createSymbolicLink(target, source)
} else {
log.debug("{} link already exists from {} to {}", type, source, target)
}
}

protected fun pathStructure(it: LibraryItem, leavingSoonParentPath: Path): PathStructure {
val rootPath = Path.of(it.rootFolderPath)
val itemFilePath = Path.of(it.filePath)
val itemFolderName = itemFilePath.subtract(rootPath).firstOrNull()

val fileOrFolder = itemFilePath.subtract(Path.of(it.parentPath)).firstOrNull() // contains filename and folder before it e.g. (Season 05) (ShowName-Episode01.mkv) or MovieName2013.mkv

val sourceFolder = rootPath.resolve(itemFolderName)
val sourceFile = sourceFolder.resolve(fileOrFolder)

val targetFolder = leavingSoonParentPath.resolve(itemFolderName)
val targetFile = targetFolder.resolve(fileOrFolder)

return PathStructure(sourceFolder, sourceFile, targetFolder, targetFile)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,33 @@ package com.github.schaka.janitorr.mediaserver.emby
import com.github.schaka.janitorr.ApplicationProperties
import com.github.schaka.janitorr.FileSystemProperties
import com.github.schaka.janitorr.mediaserver.MediaServerService
import com.github.schaka.janitorr.mediaserver.jellyfin.JellyfinRestService
import com.github.schaka.janitorr.mediaserver.jellyfin.library.AddLibraryRequest
import com.github.schaka.janitorr.mediaserver.jellyfin.library.LibraryType
import com.github.schaka.janitorr.servarr.LibraryItem
import org.slf4j.LoggerFactory
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.stereotype.Service
import org.springframework.util.FileSystemUtils
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.exists
import kotlin.io.path.listDirectoryEntries

@Service
@ConditionalOnProperty("clients.emby.enabled", havingValue = "true")
class EmbyRestService(

val jellyfinProperties: EmbyProperties,
val embyProperties: EmbyProperties,
val applicationProperties: ApplicationProperties,
val fileSystemProperties: FileSystemProperties
val fileSystemProperties: FileSystemProperties,
val embyClient: EmbyClient,
val embyUserClient: EmbyUserClient

) : MediaServerService {
) : MediaServerService() {

companion object {
private val log = LoggerFactory.getLogger(this::class.java.enclosingClass)
private val seasonPattern = Regex("Season (?<season>\\d+)")
private val filePattern = Regex("^.*\\.(mkv|mp4|avi|webm|mts|m2ts|ts|wmv|mpg|mpeg|mp2|m2v|m4v)\$")
private val numberPattern = Regex("[0-9]+")
}

override fun cleanupTvShows(items: List<LibraryItem>) {
Expand All @@ -33,6 +39,79 @@ class EmbyRestService(
}

override fun updateGoneSoon(type: LibraryType, items: List<LibraryItem>, onlyAddLinks: Boolean) {

// Only do this, if we can get access to the file system to create a link structure
if (!fileSystemProperties.access || fileSystemProperties.leavingSoonDir == null) {
return
}

val result = embyClient.listLibraries()
val collectionFilter = type.collectionType.lowercase()
// subdirectory (i.e. /leaving-soon/tv
val path = Path.of(fileSystemProperties.leavingSoonDir, type.folderName)

// Collections are created via the Collection API, but it just puts them into a BoxSet library called collections
// They're also a lot harder (imho) to manage - so we just create a media library that consists only
var goneSoonCollection = result.firstOrNull { it.CollectionType == collectionFilter && it.Name == "${type.collectionName} (Deleted Soon)" }
if (goneSoonCollection == null) {
Files.createDirectories(path)
embyClient.createLibrary("${type.collectionName} (Deleted Soon)", type.collectionType, AddLibraryRequest(), listOf(path.toUri().path))
goneSoonCollection = embyClient.listLibraries().firstOrNull { it.CollectionType == collectionFilter && it.Name == "${type.collectionName} (Deleted Soon)" }
}

// Clean up entire directory and rebuild from scratch - this can help with clearing orphaned data
if (fileSystemProperties.fromScratch && !onlyAddLinks) {
FileSystemUtils.deleteRecursively(path)
Files.createDirectories(path)
}

items.forEach {
try {

// FIXME: Figure out if we're dealing with single episodes in a season when season folders are deactivated in Sonarr
// Idea: If we did have an item for every episode in a season, this might work
// For now, just assume season folders are always activated
val structure = pathStructure(it, path)

if (type == LibraryType.TV_SHOWS && it.season != null && !isMediaFile(structure.sourceFile.toString())) {
// TV Shows
val sourceSeasonFolder = structure.sourceFile
val targetSeasonFolder = structure.targetFile
log.trace("Season folder - Source: {}, Target: {}", sourceSeasonFolder, targetSeasonFolder)

if (sourceSeasonFolder.exists()) {
log.trace("Creating season folder", targetSeasonFolder)
Files.createDirectories(targetSeasonFolder)

val files = sourceSeasonFolder.listDirectoryEntries().filter { f -> isMediaFile(f.toString()) }
for (file in files) {
val fileName = file.subtract(sourceSeasonFolder).firstOrNull()!!

val source = sourceSeasonFolder.resolve(fileName)
val target = targetSeasonFolder.resolve(fileName)
createSymLink(source, target, "episode")
}
} else {
log.info("Can't find original season folder - no links to create {}", sourceSeasonFolder)
}
} else if (type == LibraryType.MOVIES) {
// Movies
val source = structure.sourceFile
log.trace("Movie folder - {}", structure)

if (source.exists()) {
val target = structure.targetFile
Files.createDirectories(structure.targetFolder)
createSymLink(source, target, "movie")
}
else {
log.info("Can't find original movie folder - no links to create {}", source)
}
}
} catch (e: Exception) {
log.error("Couldn't find path {}", it.parentPath)
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,16 @@ import kotlin.io.path.*
@ConditionalOnProperty("clients.jellyfin.enabled", havingValue = "true")
class JellyfinRestService(

val jellyfinClient: com.github.schaka.janitorr.mediaserver.jellyfin.JellyfinClient,
val jellyfinClient: JellyfinClient,
val jellyfinUserClient: JellyfinUserClient,
val jellyfinProperties: JellyfinProperties,
val applicationProperties: ApplicationProperties,
val fileSystemProperties: FileSystemProperties

) : MediaServerService {
) : MediaServerService() {

companion object {
private val log = LoggerFactory.getLogger(this::class.java.enclosingClass)
private val seasonPattern = Regex("Season (?<season>\\d+)")
private val filePattern = Regex("^.*\\.(mkv|mp4|avi|webm|mts|m2ts|ts|wmv|mpg|mpeg|mp2|m2v|m4v)\$")
private val numberPattern = Regex("[0-9]+")
}

override fun cleanupTvShows(items: List<LibraryItem>) {
Expand Down Expand Up @@ -110,15 +107,6 @@ class JellyfinRestService(
}
}

fun parseMetadataId(value: String?): Int? {
return value?.let {
numberPattern.findAll(it)
.map(MatchResult::value)
.map(String::toInt)
.firstOrNull()
}
}

override fun updateGoneSoon(type: LibraryType, items: List<LibraryItem>, onlyAddLinks: Boolean) {

// Only do this, if we can get access to the file system to create a link structure
Expand Down Expand Up @@ -195,32 +183,6 @@ class JellyfinRestService(
}
}

private fun isMediaFile(path: String) =
filePattern.matches(path)

private fun createSymLink(source: Path, target: Path, type: String) {
if (!Files.exists(target)) {
log.debug("Creating {} link from {} to {}", type, source, target)
Files.createSymbolicLink(target, source)
} else {
log.debug("{} link already exists from {} to {}", type, source, target)
}
}

fun pathStructure(it: LibraryItem, leavingSoonParentPath: Path): PathStructure {
val rootPath = Path.of(it.rootFolderPath)
val itemFilePath = Path.of(it.filePath)
val itemFolderName = itemFilePath.subtract(rootPath).firstOrNull()

val fileOrFolder = itemFilePath.subtract(Path.of(it.parentPath)).firstOrNull() // contains filename and folder before it e.g. (Season 05) (ShowName-Episode01.mkv) or MovieName2013.mkv

val sourceFolder = rootPath.resolve(itemFolderName)
val sourceFile = sourceFolder.resolve(fileOrFolder)

val targetFolder = leavingSoonParentPath.resolve(itemFolderName)
val targetFile = targetFolder.resolve(fileOrFolder)

return PathStructure(sourceFolder, sourceFile, targetFolder, targetFile)
}

}

0 comments on commit 22e1b82

Please sign in to comment.