Skip to content

Commit

Permalink
[Architecture] Cleanup and preparation for Emby
Browse files Browse the repository at this point in the history
  • Loading branch information
Schaka committed Mar 6, 2024
1 parent 7e870de commit ae9f953
Show file tree
Hide file tree
Showing 56 changed files with 222 additions and 458 deletions.
22 changes: 10 additions & 12 deletions src/main/kotlin/com/github/schaka/janitorr/CleanupSchedule.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package com.github.schaka.janitorr

import com.github.schaka.janitorr.jellyfin.JellyfinService
import com.github.schaka.janitorr.jellyfin.library.LibraryType
import com.github.schaka.janitorr.jellyfin.library.LibraryType.MOVIES
import com.github.schaka.janitorr.jellyfin.library.LibraryType.TV_SHOWS
import com.github.schaka.janitorr.jellyseerr.JellyseerrRestService
import com.github.schaka.janitorr.mediaserver.jellyfin.library.LibraryType.MOVIES
import com.github.schaka.janitorr.mediaserver.jellyfin.library.LibraryType.TV_SHOWS
import com.github.schaka.janitorr.jellyseerr.JellyseerrService
import com.github.schaka.janitorr.mediaserver.MediaServerService
import com.github.schaka.janitorr.servarr.LibraryItem
import com.github.schaka.janitorr.servarr.radarr.RadarrService
import com.github.schaka.janitorr.servarr.sonarr.SonarrService
Expand All @@ -15,7 +13,7 @@ import java.time.LocalDateTime

@Service
class CleanupSchedule(
val jellyfinService: JellyfinService,
val mediaServerService: MediaServerService,
val jellyseerrService: JellyseerrService,
val applicationProperties: ApplicationProperties,
val sonarrService: SonarrService,
Expand All @@ -33,14 +31,14 @@ class CleanupSchedule(

val sonarrShows = sonarrService.getEntries()
val leavingShows = sonarrShows.filter { it.date.plusDays(seasonExpiration - leavingSoonExpiration) < today && it.date.plusDays(seasonExpiration) >= today }
jellyfinService.updateGoneSoon(TV_SHOWS, leavingShows)
mediaServerService.updateGoneSoon(TV_SHOWS, leavingShows)

val toDeleteShows = sonarrShows.filter { it.date.plusDays(seasonExpiration) < today }
deleteTvShows(toDeleteShows)

val radarrMovies = radarrService.getEntries()
val leavingSoonMovies = radarrMovies.filter { it.date.plusDays(movieExpiration - leavingSoonExpiration) < today && it.date.plusDays(movieExpiration) >= today }
jellyfinService.updateGoneSoon(MOVIES, leavingSoonMovies)
mediaServerService.updateGoneSoon(MOVIES, leavingSoonMovies)

val toDeleteMovies = radarrMovies.filter { it.date.plusDays(movieExpiration) < today }
deleteMovies(toDeleteMovies)
Expand All @@ -53,8 +51,8 @@ class CleanupSchedule(
val deletedMovies = toDeleteMovies.filter { !it.seeding }

jellyseerrService.cleanupRequests(deletedMovies)
jellyfinService.cleanupMovies(deletedMovies)
jellyfinService.updateGoneSoon(MOVIES, cannotDeleteMovies, true)
mediaServerService.cleanupMovies(deletedMovies)
mediaServerService.updateGoneSoon(MOVIES, cannotDeleteMovies, true)
}

private fun deleteTvShows(toDeleteShows: List<LibraryItem>) {
Expand All @@ -64,7 +62,7 @@ class CleanupSchedule(
val deletedShows = toDeleteShows.filter { !it.seeding }

jellyseerrService.cleanupRequests(deletedShows)
jellyfinService.cleanupTvShows(deletedShows)
jellyfinService.updateGoneSoon(TV_SHOWS, cannotDeleteShow, true)
mediaServerService.cleanupTvShows(deletedShows)
mediaServerService.updateGoneSoon(TV_SHOWS, cannotDeleteShow, true)
}
}
9 changes: 0 additions & 9 deletions src/main/kotlin/com/github/schaka/janitorr/TestController.kt
Original file line number Diff line number Diff line change
@@ -1,19 +1,10 @@
package com.github.schaka.janitorr

import com.github.schaka.janitorr.jellyfin.JellyfinRestService
import com.github.schaka.janitorr.jellyfin.JellyfinService
import com.github.schaka.janitorr.jellyfin.library.LibraryType
import com.github.schaka.janitorr.jellyseerr.JellyseerrRestService
import com.github.schaka.janitorr.jellyseerr.JellyseerrService
import com.github.schaka.janitorr.servarr.radarr.RadarrService
import com.github.schaka.janitorr.servarr.sonarr.SonarrService
import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import java.time.LocalDateTime

@Controller
@RequestMapping("/hook")
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.github.schaka.janitorr.jellyseerr

import com.github.schaka.janitorr.jellyfin.library.VirtualFolderResponse
import com.github.schaka.janitorr.jellyseerr.paging.JellyseerrPage
import com.github.schaka.janitorr.jellyseerr.requests.RequestResponse
import feign.Param
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.github.schaka.janitorr.jellyseerr

import com.github.schaka.janitorr.jellyfin.library.LibraryType
import com.github.schaka.janitorr.servarr.LibraryItem
import org.slf4j.LoggerFactory
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,32 @@
package com.github.schaka.janitorr.jellyfin
package com.github.schaka.janitorr.mediaserver

import com.github.schaka.janitorr.jellyfin.library.LibraryType
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.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.stereotype.Service

/**
* Does nothing. Used in case the user does not supply Jellyfin configuration.
*/
@Service
@ConditionalOnProperty("clients.jellyfin.enabled", havingValue = "false", matchIfMissing = true)
class JellyfinNoOpService : JellyfinService {
@ConditionalOnProperty(value = ["clients.emby.enabled", "clients.jellyfin.enabled"], havingValue = "false")
class MediaServerNoOpService : MediaServerService {

companion object {
private val log = LoggerFactory.getLogger(this::class.java.enclosingClass)
}

override fun cleanupTvShows(items: List<LibraryItem>) {
log.info("Jellyfin not implemented. No TV shows deleted.")
log.info("Media Server not implemented. No TV shows deleted.")
}

override fun cleanupMovies(items: List<LibraryItem>) {
log.info("Jellyfin not implemented. No movies deleted.")
log.info("Media Server not implemented. No movies deleted.")
}

override fun updateGoneSoon(type: LibraryType, items: List<LibraryItem>, onlyAddLinks: Boolean) {
log.info("Jellyfin not implemented. No 'Gone Soon' library created.")
log.info("Media Server not implemented. No 'Gone Soon' library created.")
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.github.schaka.janitorr.jellyfin
package com.github.schaka.janitorr.mediaserver

import com.github.schaka.janitorr.jellyfin.library.LibraryType
import com.github.schaka.janitorr.mediaserver.jellyfin.library.LibraryType
import com.github.schaka.janitorr.servarr.LibraryItem

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

fun cleanupMovies(items: List<LibraryItem>)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.github.schaka.janitorr.mediaserver.emby

import com.github.schaka.janitorr.mediaserver.jellyfin.api.User
import com.github.schaka.janitorr.mediaserver.jellyfin.library.*
import com.github.schaka.janitorr.mediaserver.jellyfin.library.items.ItemPage
import com.github.schaka.janitorr.mediaserver.jellyfin.library.items.MediaFolderItem
import feign.Param
import feign.RequestLine

interface EmbyClient {

@RequestLine("GET /Users")
fun listUsers(): List<User>

@RequestLine("GET /Library/VirtualFolders")
fun listLibraries(): List<VirtualFolderResponse>

@RequestLine("POST /Library/VirtualFolders?name={name}&collectionType={type}&paths={paths}&refreshLibrary=false")
fun createLibrary(@Param("name") name: String, @Param("type") collectionType: String, request: AddLibraryRequest, @Param("paths") paths: List<String> = listOf())

@RequestLine("POST /Collections?name={name}&ids&parentId={parentId}&isLocked=true")
fun createCollection(@Param("name") name: String, @Param("parentId") parentId: String? = null): CollectionResponse

@RequestLine("POST /Library/VirtualFolders/Paths?refreshLibrary={refresh}")
fun addPathToLibrary(request: AddPathRequest, @Param("refresh") refresh: Boolean = true)

@RequestLine("DELETE /Library/VirtualFolders/Paths?name={name}&path={path}&refreshLibrary={refresh}")
fun removePathFromLibrary(@Param("name") name: String, @Param("path") path: String, @Param("refresh") refresh: Boolean = true)

@RequestLine("POST /Collections/{id}/Items?ids={itemIds}")
fun addItemToCollection(@Param("id") id: String, itemIds: List<String>)

@RequestLine("GET /Library/MediaFolders")
fun getAllItems(): ItemPage<MediaFolderItem>

@RequestLine("GET /Items?limit=10000&includeItemTypes=Series&parentId={parentId}&fields=Path,ProviderIds")
fun getAllTvShows(@Param("parentId") parentId: String): ItemPage<LibraryContent>

@RequestLine("GET /Items?limit=10000&includeItemTypes=Movies&parentId={parentId}&fields=Path,ProviderIds")
fun getAllMovies(@Param("parentId") parentId: String): ItemPage<LibraryContent>


@RequestLine("GET /Shows/{tvshow}/Seasons?fields=Path,ProviderIds")
fun getAllSeasons(@Param("tvshow") showId: String): ItemPage<LibraryContent>

@RequestLine("GET /Movies/{movieId}?fields=Path,ProviderIds")
fun getMovie(@Param("movieId") movieId: String): ItemPage<LibraryContent>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.github.schaka.janitorr.mediaserver.emby

import com.fasterxml.jackson.databind.ObjectMapper
import feign.Feign
import feign.jackson.JacksonDecoder
import feign.jackson.JacksonEncoder
import org.slf4j.LoggerFactory
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpHeaders.AUTHORIZATION
import org.springframework.http.HttpHeaders.CONTENT_TYPE
import org.springframework.http.MediaType.APPLICATION_JSON_VALUE

@Configuration
@ConditionalOnProperty("clients.emby.enabled", havingValue = "true")
class EmbyClientConfig {

companion object {
private val log = LoggerFactory.getLogger(this::class.java.enclosingClass)
private val janitorrClientString = "Client=\"Janitorr\", Device=\"Spring Boot\", DeviceId=\"Janitorr-Device-Id\", Version=\"1.0\""
}

@Bean
fun embyClient(properties: EmbyProperties, mapper: ObjectMapper): EmbyClient {
return Feign.builder()
.decoder(JacksonDecoder(mapper))
.encoder(JacksonEncoder(mapper))
.requestInterceptor {
it.header(AUTHORIZATION, "MediaBrowser Token=\"${properties.apiKey}\", ${janitorrClientString}")
it.header(CONTENT_TYPE, APPLICATION_JSON_VALUE)
}
.target(EmbyClient::class.java, properties.url)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.github.schaka.janitorr.mediaserver.emby

import org.springframework.boot.context.properties.ConfigurationProperties

@ConfigurationProperties(prefix = "clients.emby")
data class EmbyProperties(
val url: String,
val apiKey: String,
val username: String,
val password: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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.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

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

val jellyfinProperties: EmbyProperties,
val applicationProperties: ApplicationProperties,
val fileSystemProperties: FileSystemProperties

) : 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>) {
}

override fun cleanupMovies(items: List<LibraryItem>) {
}

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

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.github.schaka.janitorr.jellyfin
package com.github.schaka.janitorr.mediaserver.jellyfin

import org.springframework.beans.factory.annotation.Qualifier

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.github.schaka.janitorr.jellyfin
package com.github.schaka.janitorr.mediaserver.jellyfin

import com.github.schaka.janitorr.jellyfin.api.User
import com.github.schaka.janitorr.jellyfin.library.*
import com.github.schaka.janitorr.jellyfin.library.items.ItemPage
import com.github.schaka.janitorr.jellyfin.library.items.MediaFolderItem
import com.github.schaka.janitorr.mediaserver.jellyfin.api.User
import com.github.schaka.janitorr.mediaserver.jellyfin.library.*
import com.github.schaka.janitorr.mediaserver.jellyfin.library.items.ItemPage
import com.github.schaka.janitorr.mediaserver.jellyfin.library.items.MediaFolderItem
import feign.Param
import feign.RequestLine

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.github.schaka.janitorr.jellyfin
package com.github.schaka.janitorr.mediaserver.jellyfin

import com.fasterxml.jackson.databind.ObjectMapper
import feign.Feign
Expand Down Expand Up @@ -30,38 +30,38 @@ class JellyfinClientConfig {
private val janitorrClientString = "Client=\"Janitorr\", Device=\"Spring Boot\", DeviceId=\"Janitorr-Device-Id\", Version=\"1.0\""
}

@Jellyfin
@com.github.schaka.janitorr.mediaserver.jellyfin.Jellyfin
@Bean
fun jellyfinRestTemplate(builder: RestTemplateBuilder, properties: JellyfinProperties): RestTemplate {
fun jellyfinRestTemplate(builder: RestTemplateBuilder, properties: com.github.schaka.janitorr.mediaserver.jellyfin.JellyfinProperties): RestTemplate {
return builder
.rootUri("${properties.url}/")
.defaultHeader(AUTHORIZATION, "MediaBrowser Token=\"${properties.apiKey}\", $janitorrClientString")
.defaultHeader(AUTHORIZATION, "MediaBrowser Token=\"${properties.apiKey}\", ${com.github.schaka.janitorr.mediaserver.jellyfin.JellyfinClientConfig.Companion.janitorrClientString}")
.build()
}

@Bean
fun jellyfinClient(properties: JellyfinProperties, mapper: ObjectMapper): JellyfinClient {
fun jellyfinClient(properties: com.github.schaka.janitorr.mediaserver.jellyfin.JellyfinProperties, mapper: ObjectMapper): com.github.schaka.janitorr.mediaserver.jellyfin.JellyfinClient {
return Feign.builder()
.decoder(JacksonDecoder(mapper))
.encoder(JacksonEncoder(mapper))
.requestInterceptor {
it.header(AUTHORIZATION, "MediaBrowser Token=\"${properties.apiKey}\", $janitorrClientString")
it.header(AUTHORIZATION, "MediaBrowser Token=\"${properties.apiKey}\", ${com.github.schaka.janitorr.mediaserver.jellyfin.JellyfinClientConfig.Companion.janitorrClientString}")
it.header(CONTENT_TYPE, APPLICATION_JSON_VALUE)
}
.target(JellyfinClient::class.java, properties.url)
.target(com.github.schaka.janitorr.mediaserver.jellyfin.JellyfinClient::class.java, properties.url)
}

@Bean
fun jellyfinUserClient(properties: JellyfinProperties, mapper: ObjectMapper): JellyfinUserClient {
fun jellyfinUserClient(properties: com.github.schaka.janitorr.mediaserver.jellyfin.JellyfinProperties, mapper: ObjectMapper): com.github.schaka.janitorr.mediaserver.jellyfin.JellyfinUserClient {
return Feign.builder()
.decoder(JacksonDecoder(mapper))
.encoder(JacksonEncoder(mapper))
.requestInterceptor(JellyfinUserInterceptor(properties))
.target(JellyfinUserClient::class.java, properties.url)
.requestInterceptor(com.github.schaka.janitorr.mediaserver.jellyfin.JellyfinClientConfig.JellyfinUserInterceptor(properties))
.target(com.github.schaka.janitorr.mediaserver.jellyfin.JellyfinUserClient::class.java, properties.url)
}

private class JellyfinUserInterceptor(
val properties: JellyfinProperties
val properties: com.github.schaka.janitorr.mediaserver.jellyfin.JellyfinProperties
) : RequestInterceptor {

var lastUpdate: LocalDateTime = LocalDateTime.MIN
Expand All @@ -73,17 +73,17 @@ class JellyfinClientConfig {
val userInfo = getUserInfo(properties)
accessToken = userInfo.body?.get("AccessToken").toString()
lastUpdate = LocalDateTime.now()
log.info("Logged in to Jellyfin as {} {}", properties.username, accessToken)
com.github.schaka.janitorr.mediaserver.jellyfin.JellyfinClientConfig.Companion.log.info("Logged in to Jellyfin as {} {}", properties.username, accessToken)
}

template.header(AUTHORIZATION, "MediaBrowser Token=\"${accessToken}\", $janitorrClientString")
template.header(AUTHORIZATION, "MediaBrowser Token=\"${accessToken}\", ${com.github.schaka.janitorr.mediaserver.jellyfin.JellyfinClientConfig.Companion.janitorrClientString}")
template.header(CONTENT_TYPE, APPLICATION_JSON_VALUE)
}

private fun getUserInfo(properties: JellyfinProperties): ResponseEntity<Map<*, *>> {
private fun getUserInfo(properties: com.github.schaka.janitorr.mediaserver.jellyfin.JellyfinProperties): ResponseEntity<Map<*, *>> {
val login = RestTemplate()
val headers = HttpHeaders()
headers.set(AUTHORIZATION, "MediaBrowser , $janitorrClientString")
headers.set(AUTHORIZATION, "MediaBrowser , ${com.github.schaka.janitorr.mediaserver.jellyfin.JellyfinClientConfig.Companion.janitorrClientString}")
headers.set(CONTENT_TYPE, APPLICATION_JSON_VALUE)
val content = object {
val Username = properties.username
Expand Down
Loading

0 comments on commit ae9f953

Please sign in to comment.