From 8fdb6ac24dac5b0905658500887c66b943097995 Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Sat, 4 Jan 2025 18:26:11 +0100 Subject: [PATCH] Preparations for plugin prioritisation and metadata merging --- build.gradle.kts | 2 +- .../management/GameyfinPluginManager.kt | 11 +-- .../management/PluginManagementEntry.kt | 11 +-- .../de/grimsi/gameyfin/games/GameService.kt | 79 +++++++++++-------- .../de/grimsi/gameyfin/games/dto/GameDto.kt | 1 + .../de/grimsi/gameyfin/games/entities/Game.kt | 28 +++---- .../pluginapi/gamemetadata/GameMetadata.kt | 32 ++++---- .../gameyfin/plugins/steam/SteamPlugin.kt | 33 +++----- .../plugins/steam/dto/SteamGameDetails.kt | 2 +- .../plugins/steam/mapper/GenreMapper.kt | 39 --------- .../gameyfin/plugins/steam/mapper/Mapper.kt | 43 ++++++++++ 11 files changed, 140 insertions(+), 141 deletions(-) delete mode 100644 plugins/steam/src/main/kotlin/de/grimsi/gameyfin/plugins/steam/mapper/GenreMapper.kt create mode 100644 plugins/steam/src/main/kotlin/de/grimsi/gameyfin/plugins/steam/mapper/Mapper.kt diff --git a/build.gradle.kts b/build.gradle.kts index 0190f5d..1cc72cb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,8 +4,8 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile allprojects { repositories { - mavenLocal() mavenCentral() + mavenLocal() } } diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/GameyfinPluginManager.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/GameyfinPluginManager.kt index 6f5b313..93b5d4e 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/GameyfinPluginManager.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/GameyfinPluginManager.kt @@ -14,7 +14,8 @@ import kotlin.io.path.Path @Component class GameyfinPluginManager( val pluginConfigRepository: PluginConfigRepository, - val dbPluginStatusProvider: DatabasePluginStatusProvider + val dbPluginStatusProvider: DatabasePluginStatusProvider, + val pluginManagementRepository: PluginManagementRepository ) : DefaultPluginManager(Path(System.getProperty("pf4j.pluginsDir", "plugins"))) { private val log = KotlinLogging.logger {} @@ -55,8 +56,8 @@ class GameyfinPluginManager( override fun loadPluginFromPath(pluginPath: Path?): PluginWrapper? { val pluginWrapper = super.loadPluginFromPath(pluginPath) - // Inject config after loading, before starting if (pluginWrapper != null) { + // Inject config after loading, before starting configurePlugin(pluginWrapper) } @@ -64,7 +65,7 @@ class GameyfinPluginManager( } override fun startPlugin(pluginId: String?): PluginState? { - if(pluginId == null) return PluginState.FAILED + if (pluginId == null) return PluginState.FAILED // Validate config before starting the plugin if (!validatePluginConfig(pluginId)) { @@ -94,7 +95,7 @@ class GameyfinPluginManager( } try { - log.info { "Start plugin '${getPluginLabel(pluginWrapper.descriptor)}'"} + log.info { "Start plugin '${getPluginLabel(pluginWrapper.descriptor)}'" } pluginWrapper.plugin.start() pluginWrapper.pluginState = PluginState.STARTED pluginWrapper.failedException = null @@ -102,7 +103,7 @@ class GameyfinPluginManager( } catch (e: Exception) { pluginWrapper.pluginState = PluginState.FAILED pluginWrapper.failedException = e - log.error { "Unable to start plugin '${getPluginLabel(pluginWrapper.descriptor)}': $e"} + log.error { "Unable to start plugin '${getPluginLabel(pluginWrapper.descriptor)}': $e" } } finally { firePluginStateEvent(PluginStateEvent(this, pluginWrapper, pluginState)) } diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/PluginManagementEntry.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/PluginManagementEntry.kt index 39d5410..0d4ba4a 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/PluginManagementEntry.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/PluginManagementEntry.kt @@ -1,19 +1,14 @@ package de.grimsi.gameyfin.core.plugins.management -import jakarta.persistence.Column import jakarta.persistence.Entity import jakarta.persistence.Id -import jakarta.persistence.Table -import jakarta.validation.constraints.NotNull @Entity -@Table(name = "plugin_management") data class PluginManagementEntry( @Id - @Column(name = "plugin_id") val pluginId: String, - @NotNull - @Column(name = "enabled") - var enabled: Boolean = true + var enabled: Boolean = true, + + var priority: Int = Int.MAX_VALUE ) \ No newline at end of file diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameService.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameService.kt index f2318e0..dc810f8 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameService.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameService.kt @@ -17,7 +17,7 @@ import kotlinx.coroutines.runBlocking import org.pf4j.PluginManager import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service -import java.net.URL +import java.net.URI import java.net.URLConnection import java.nio.file.Path @@ -43,20 +43,7 @@ class GameService( } fun createFromFile(path: Path): GameDto { - val metadataResults: Map = runBlocking { - coroutineScope { - metadataPlugins.associateWith { - async { - try { - it.fetchMetadata(path.fileName.toString()) - } catch (e: Exception) { - log.error(e) { "Error fetching metadata with plugin ${it.javaClass.name}" } - null - } - }.await() - } - } - } + val metadataResults = queryPlugins(path.fileName.toString()) val (plugin, metadata) = metadataResults.entries.firstOrNull { it.value != null } ?: throw NoMatchException("Could not match game at $path") @@ -84,25 +71,48 @@ class GameService( return gameRepository.findByIdOrNull(id) ?: throw IllegalArgumentException("Game with id $id not found") } + /** + * Queries all metadata plugins for metadata on the provided game title + * Runs the queries concurrently and asynchronously + * @return A map of metadata plugins and their respective results + */ + private fun queryPlugins(gameTitle: String): Map { + return runBlocking { + coroutineScope { + metadataPlugins.associateWith { + async { + try { + it.fetchMetadata(gameTitle) + } catch (e: Exception) { + log.error(e) { "Error fetching metadata with plugin ${it.javaClass.name}" } + null + } + }.await() + } + } + } + } + private fun toDto(game: Game): GameDto { val gameId = game.id ?: throw IllegalArgumentException("Game ID is null") return GameDto( id = gameId, title = game.title, - coverId = game.coverImage.id, + coverId = game.coverImage?.id, comment = game.comment, summary = game.summary, release = game.release, - publishers = game.publishers.map { it.name }, - developers = game.developers.map { it.name }, - genres = game.genres.map { it.name }, - themes = game.themes.map { it.name }, - keywords = game.keywords.toList(), - features = game.features.map { it.name }, - perspectives = game.perspectives.map { it.name }, - imageIds = game.images.mapNotNull { it.id }, - videoUrls = game.videoUrls.map { it.toString() }, + publishers = game.publishers?.map { it.name }, + developers = game.developers?.map { it.name }, + genres = game.genres?.map { it.name }, + themes = game.themes?.map { it.name }, + keywords = game.keywords?.toList(), + features = game.features?.map { it.name }, + perspectives = game.perspectives?.map { it.name }, + imageIds = game.images?.mapNotNull { it.id }, + videoUrls = game.videoUrls?.map { it.toString() }, + path = game.path, metadata = toDto(game.metadata) ) } @@ -124,16 +134,16 @@ class GameService( return Game( title = metadata.title, summary = metadata.description, - coverImage = downloadAndPersist(metadata.coverUrl, ImageType.COVER), + coverImage = metadata.coverUrl?.let { downloadAndPersist(it, ImageType.COVER) }, release = metadata.release, - publishers = metadata.publishedBy.map { toEntity(it, CompanyType.PUBLISHER) }.toSet(), - developers = metadata.developedBy.map { toEntity(it, CompanyType.DEVELOPER) }.toSet(), + publishers = metadata.publishedBy?.map { toEntity(it, CompanyType.PUBLISHER) }?.toSet(), + developers = metadata.developedBy?.map { toEntity(it, CompanyType.DEVELOPER) }?.toSet(), genres = metadata.genres, themes = metadata.themes, keywords = metadata.keywords, features = metadata.features, perspectives = metadata.perspectives, - images = metadata.screenshotUrls.map { downloadAndPersist(it, ImageType.SCREENSHOT) }.toSet(), + images = metadata.screenshotUrls?.map { downloadAndPersist(it, ImageType.SCREENSHOT) }?.toSet(), videoUrls = metadata.videoUrls, path = path.toString(), metadata = mapOf("title" to FieldMetadata(sourcePlugin)) @@ -146,12 +156,13 @@ class GameService( return companyRepository.save(company) } - private fun downloadAndPersist(imageUrl: URL, type: ImageType): Image { - imageRepository.findByOriginalUrl(imageUrl)?.let { return it } + private fun downloadAndPersist(imageUrl: URI, type: ImageType): Image { + val parsedUrl = imageUrl.toURL() + imageRepository.findByOriginalUrl(parsedUrl)?.let { return it } - val image = Image(originalUrl = imageUrl, type = type) - imageUrl.openStream().use { input -> - image.mimeType = URLConnection.guessContentTypeFromName(imageUrl.file) + val image = Image(originalUrl = parsedUrl, type = type) + parsedUrl.openStream().use { input -> + image.mimeType = URLConnection.guessContentTypeFromName(parsedUrl.file) imageContentStore.setContent(image, input) } return imageRepository.save(image) diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/dto/GameDto.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/dto/GameDto.kt index f529a7b..5e48864 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/dto/GameDto.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/dto/GameDto.kt @@ -18,5 +18,6 @@ class GameDto( val perspectives: List?, val imageIds: List?, val videoUrls: List?, + val path: String, val metadata: Map ) \ No newline at end of file diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/entities/Game.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/entities/Game.kt index 7d3494c..9ae7c7b 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/entities/Game.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/entities/Game.kt @@ -5,7 +5,7 @@ import de.grimsi.gameyfin.pluginapi.gamemetadata.Genre import de.grimsi.gameyfin.pluginapi.gamemetadata.PlayerPerspective import de.grimsi.gameyfin.pluginapi.gamemetadata.Theme import jakarta.persistence.* -import java.net.URL +import java.net.URI import java.time.Instant @Entity @@ -17,7 +17,7 @@ class Game( val title: String, @OneToOne(cascade = [CascadeType.MERGE]) - val coverImage: Image, + val coverImage: Image? = null, @Lob @Column(columnDefinition = "CLOB") @@ -25,40 +25,40 @@ class Game( @Lob @Column(columnDefinition = "CLOB") - val summary: String, + val summary: String? = null, - val release: Instant, + val release: Instant? = null, @ManyToMany(cascade = [CascadeType.MERGE]) - val publishers: Set, + val publishers: Set? = null, @ManyToMany(cascade = [CascadeType.MERGE]) - val developers: Set, + val developers: Set? = null, @ElementCollection - val genres: Set, + val genres: Set? = null, @ElementCollection - val themes: Set, + val themes: Set? = null, @ElementCollection - val keywords: Set, + val keywords: Set? = null, @ElementCollection - val features: Set, + val features: Set? = null, @ElementCollection - val perspectives: Set, + val perspectives: Set? = null, @OneToMany(cascade = [CascadeType.MERGE]) - val images: Set, + val images: Set? = null, @ElementCollection - val videoUrls: Set, + val videoUrls: Set? = null, @Column(unique = true) val path: String, @OneToMany(cascade = [CascadeType.ALL], orphanRemoval = true) - val metadata: Map + val metadata: Map = emptyMap() ) \ No newline at end of file diff --git a/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/gamemetadata/GameMetadata.kt b/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/gamemetadata/GameMetadata.kt index a360ac3..6b78b23 100644 --- a/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/gamemetadata/GameMetadata.kt +++ b/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/gamemetadata/GameMetadata.kt @@ -1,28 +1,29 @@ package de.grimsi.gameyfin.pluginapi.gamemetadata -import java.net.URL +import java.net.URI import java.time.Instant class GameMetadata( val title: String, - val description: String, - val coverUrl: URL, - val release: Instant, - val userRating: Int?, - val criticRating: Int?, - val developedBy: Set, - val publishedBy: Set, - val genres: Set, - val themes: Set, - val keywords: Set, - val screenshotUrls: Set, - val videoUrls: Set, - val features: Set, - val perspectives: Set + val description: String? = null, + val coverUrl: URI? = null, + val release: Instant? = null, + val userRating: Int? = null, + val criticRating: Int? = null, + val developedBy: Set? = null, + val publishedBy: Set? = null, + val genres: Set? = null, + val themes: Set? = null, + val keywords: Set? = null, + val screenshotUrls: Set? = null, + val videoUrls: Set? = null, + val features: Set? = null, + val perspectives: Set? = null ) enum class Genre { UNKNOWN, + ACTION, PINBALL, ADVENTURE, INDIE, @@ -30,6 +31,7 @@ enum class Genre { VISUAL_NOVEL, CARD_AND_BOARD_GAME, MOBA, + MMO, POINT_AND_CLICK, FIGHTING, SHOOTER, diff --git a/plugins/steam/src/main/kotlin/de/grimsi/gameyfin/plugins/steam/SteamPlugin.kt b/plugins/steam/src/main/kotlin/de/grimsi/gameyfin/plugins/steam/SteamPlugin.kt index d97b74a..eb926ea 100644 --- a/plugins/steam/src/main/kotlin/de/grimsi/gameyfin/plugins/steam/SteamPlugin.kt +++ b/plugins/steam/src/main/kotlin/de/grimsi/gameyfin/plugins/steam/SteamPlugin.kt @@ -7,7 +7,7 @@ import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadataProvider import de.grimsi.gameyfin.plugins.steam.dto.SteamDetailsResultWrapper import de.grimsi.gameyfin.plugins.steam.dto.SteamGame import de.grimsi.gameyfin.plugins.steam.dto.SteamSearchResult -import de.grimsi.gameyfin.plugins.steam.mapper.toGenre +import de.grimsi.gameyfin.plugins.steam.mapper.Mapper import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.engine.cio.* @@ -18,9 +18,6 @@ import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonPrimitive import me.xdrop.fuzzywuzzy.FuzzySearch import org.pf4j.Extension import org.pf4j.PluginWrapper @@ -29,16 +26,12 @@ import org.slf4j.LoggerFactory import java.net.URI import java.net.URLEncoder import java.nio.charset.StandardCharsets -import java.time.Instant -import java.time.LocalDate -import java.time.ZoneOffset -import java.time.format.DateTimeFormatter -import java.util.* class SteamPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) { override val configMetadata: List = emptyList() override fun validateConfig(config: Map): Boolean { + // No config to validate return true } @@ -52,6 +45,10 @@ class SteamPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) { } } + /** + * The Steam Store API I am using provides far less info than IGDB for example + * See it more as a proof of concept than a fully functional plugin + **/ override fun fetchMetadata(gameId: String): GameMetadata? { val searchResult: List = runBlocking { searchStore(gameId) } if (searchResult.isEmpty()) return null @@ -91,9 +88,11 @@ class SteamPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) { val steamDetailsResultWrapper: Map = Json.decodeFromString(responseBody) if (!steamDetailsResultWrapper.containsKey(id)) return null + if (steamDetailsResultWrapper[id]?.success != true) return null val game = steamDetailsResultWrapper[id]?.data ?: return null + // This is as much as I can get from the Steam Store API val metadata = GameMetadata( title = game.name, description = game.detailedDescription, @@ -101,7 +100,7 @@ class SteamPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) { release = game.releaseDate?.date, developedBy = game.developers.toSet(), publishedBy = game.publishers.toSet(), - genres = game.genres.map { toGenre(it) }.toSet(), + genres = game.genres.map { Mapper.genre(it) }.toSet(), keywords = game.categories.mapNotNull { it.description }.toSet(), screenshotUrls = game.screenshots.map { URI(it.pathFull!!) }.toSet(), videoUrls = game.movies.map { URI(it.webm?.max!!) }.toSet() @@ -109,19 +108,5 @@ class SteamPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) { return metadata } - - private fun string(json: JsonObject, key: String): String { - return json[key]?.jsonPrimitive?.content ?: "" - } - - private fun stringList(json: JsonObject, key: String): List { - return json[key]?.jsonArray?.map { it.jsonPrimitive.content } ?: emptyList() - } - - fun date(dateString: String): Instant { - val formatter = DateTimeFormatter.ofPattern("dd MMM, yyyy", Locale.ENGLISH) - val localDate = LocalDate.parse(dateString, formatter) - return localDate.atStartOfDay().toInstant(ZoneOffset.UTC) - } } } \ No newline at end of file diff --git a/plugins/steam/src/main/kotlin/de/grimsi/gameyfin/plugins/steam/dto/SteamGameDetails.kt b/plugins/steam/src/main/kotlin/de/grimsi/gameyfin/plugins/steam/dto/SteamGameDetails.kt index f491f49..ff16c17 100644 --- a/plugins/steam/src/main/kotlin/de/grimsi/gameyfin/plugins/steam/dto/SteamGameDetails.kt +++ b/plugins/steam/src/main/kotlin/de/grimsi/gameyfin/plugins/steam/dto/SteamGameDetails.kt @@ -16,7 +16,7 @@ data class SteamGameDetails( val type: String, val name: String, @SerialName("steam_appid") val steamAppId: Int, - @SerialName("required_age") val requiredAge: String, + @SerialName("required_age") val requiredAge: Int, @SerialName("is_free") val isFree: Boolean, @SerialName("controller_support") val controllerSupport: String?, val dlc: List?, diff --git a/plugins/steam/src/main/kotlin/de/grimsi/gameyfin/plugins/steam/mapper/GenreMapper.kt b/plugins/steam/src/main/kotlin/de/grimsi/gameyfin/plugins/steam/mapper/GenreMapper.kt deleted file mode 100644 index 354c173..0000000 --- a/plugins/steam/src/main/kotlin/de/grimsi/gameyfin/plugins/steam/mapper/GenreMapper.kt +++ /dev/null @@ -1,39 +0,0 @@ -package de.grimsi.gameyfin.plugins.steam.mapper - -import de.grimsi.gameyfin.pluginapi.gamemetadata.Genre -import de.grimsi.gameyfin.plugins.steam.dto.SteamGenre - -fun toGenre(steamGenre: SteamGenre): Genre { - return when (steamGenre.id) { - 1 -> Genre.ACTION - 2 -> Genre.STRATEGY - 25 -> Genre.ADVENTURE - 23 -> Genre.INDIE - 3 -> Genre.ROLE_PLAYING - 28 -> Genre.SIMULATOR - 29 -> Genre.MMO - 9 -> Genre.RACING - 18 -> Genre.SPORT - 37 -> Genre.UNKNOWN // Free to Play doesn't match any genre directly - 51 -> Genre.UNKNOWN // Animation & Modeling doesn't match any genre directly - 58 -> Genre.UNKNOWN // Video Production doesn't match any genre directly - 4 -> Genre.UNKNOWN // Casual doesn't match any genre directly - 73 -> Genre.UNKNOWN // Violent doesn't map directly to a genre - 72 -> Genre.UNKNOWN // Nudity doesn't match any genre directly - 70 -> Genre.UNKNOWN // Early Access doesn't map directly to a genre - 74 -> Genre.UNKNOWN // Gore doesn't match any genre directly - 57 -> Genre.UNKNOWN // Utilities doesn't match any genre directly - 52 -> Genre.UNKNOWN // Audio Production doesn't match any genre directly - 53 -> Genre.UNKNOWN // Design & Illustration doesn't match any genre directly - 59 -> Genre.UNKNOWN // Web Publishing doesn't map directly to a genre - 55 -> Genre.UNKNOWN // Photo Editing doesn't map directly to a genre - 54 -> Genre.UNKNOWN // Education doesn't match any genre directly - 56 -> Genre.UNKNOWN // Software Training doesn't map directly to a genre - 71 -> Genre.UNKNOWN // Sexual Content doesn't match any genre directly - 60 -> Genre.UNKNOWN // Game Development doesn't map directly to a genre - 50 -> Genre.UNKNOWN // Accounting doesn't map directly to a genre - 81 -> Genre.UNKNOWN // Documentary doesn't map directly to a genre - 84 -> Genre.UNKNOWN // Tutorial doesn't map directly to a genre - else -> Genre.UNKNOWN - } -} \ No newline at end of file diff --git a/plugins/steam/src/main/kotlin/de/grimsi/gameyfin/plugins/steam/mapper/Mapper.kt b/plugins/steam/src/main/kotlin/de/grimsi/gameyfin/plugins/steam/mapper/Mapper.kt new file mode 100644 index 0000000..d564fee --- /dev/null +++ b/plugins/steam/src/main/kotlin/de/grimsi/gameyfin/plugins/steam/mapper/Mapper.kt @@ -0,0 +1,43 @@ +package de.grimsi.gameyfin.plugins.steam.mapper + +import de.grimsi.gameyfin.pluginapi.gamemetadata.Genre +import de.grimsi.gameyfin.plugins.steam.dto.SteamGenre + +class Mapper { + companion object { + fun genre(steamGenre: SteamGenre): Genre { + return when (steamGenre.id) { + 1 -> Genre.ACTION + 2 -> Genre.STRATEGY + 25 -> Genre.ADVENTURE + 23 -> Genre.INDIE + 3 -> Genre.ROLE_PLAYING + 28 -> Genre.SIMULATOR + 29 -> Genre.MMO + 9 -> Genre.RACING + 18 -> Genre.SPORT + 37 -> Genre.UNKNOWN // Free to Play doesn't match any genre directly + 51 -> Genre.UNKNOWN // Animation & Modeling doesn't match any genre directly + 58 -> Genre.UNKNOWN // Video Production doesn't match any genre directly + 4 -> Genre.UNKNOWN // Casual doesn't match any genre directly + 73 -> Genre.UNKNOWN // Violent doesn't map directly to a genre + 72 -> Genre.UNKNOWN // Nudity doesn't match any genre directly + 70 -> Genre.UNKNOWN // Early Access doesn't map directly to a genre + 74 -> Genre.UNKNOWN // Gore doesn't match any genre directly + 57 -> Genre.UNKNOWN // Utilities doesn't match any genre directly + 52 -> Genre.UNKNOWN // Audio Production doesn't match any genre directly + 53 -> Genre.UNKNOWN // Design & Illustration doesn't match any genre directly + 59 -> Genre.UNKNOWN // Web Publishing doesn't map directly to a genre + 55 -> Genre.UNKNOWN // Photo Editing doesn't map directly to a genre + 54 -> Genre.UNKNOWN // Education doesn't match any genre directly + 56 -> Genre.UNKNOWN // Software Training doesn't map directly to a genre + 71 -> Genre.UNKNOWN // Sexual Content doesn't match any genre directly + 60 -> Genre.UNKNOWN // Game Development doesn't map directly to a genre + 50 -> Genre.UNKNOWN // Accounting doesn't map directly to a genre + 81 -> Genre.UNKNOWN // Documentary doesn't map directly to a genre + 84 -> Genre.UNKNOWN // Tutorial doesn't map directly to a genre + else -> Genre.UNKNOWN + } + } + } +} \ No newline at end of file