mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-13 16:40:01 +00:00
Preparations for plugin prioritisation and metadata merging
This commit is contained in:
+1
-1
@@ -4,8 +4,8 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-5
@@ -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))
|
||||
}
|
||||
|
||||
+3
-8
@@ -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
|
||||
)
|
||||
@@ -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<GameMetadataProvider, GameMetadata?> = 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<GameMetadataProvider, GameMetadata?> {
|
||||
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)
|
||||
|
||||
@@ -18,5 +18,6 @@ class GameDto(
|
||||
val perspectives: List<String>?,
|
||||
val imageIds: List<Long>?,
|
||||
val videoUrls: List<String>?,
|
||||
val path: String,
|
||||
val metadata: Map<String, GameMetadataDto>
|
||||
)
|
||||
@@ -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<Company>,
|
||||
val publishers: Set<Company>? = null,
|
||||
|
||||
@ManyToMany(cascade = [CascadeType.MERGE])
|
||||
val developers: Set<Company>,
|
||||
val developers: Set<Company>? = null,
|
||||
|
||||
@ElementCollection
|
||||
val genres: Set<Genre>,
|
||||
val genres: Set<Genre>? = null,
|
||||
|
||||
@ElementCollection
|
||||
val themes: Set<Theme>,
|
||||
val themes: Set<Theme>? = null,
|
||||
|
||||
@ElementCollection
|
||||
val keywords: Set<String>,
|
||||
val keywords: Set<String>? = null,
|
||||
|
||||
@ElementCollection
|
||||
val features: Set<GameFeature>,
|
||||
val features: Set<GameFeature>? = null,
|
||||
|
||||
@ElementCollection
|
||||
val perspectives: Set<PlayerPerspective>,
|
||||
val perspectives: Set<PlayerPerspective>? = null,
|
||||
|
||||
@OneToMany(cascade = [CascadeType.MERGE])
|
||||
val images: Set<Image>,
|
||||
val images: Set<Image>? = null,
|
||||
|
||||
@ElementCollection
|
||||
val videoUrls: Set<URL>,
|
||||
val videoUrls: Set<URI>? = null,
|
||||
|
||||
@Column(unique = true)
|
||||
val path: String,
|
||||
|
||||
@OneToMany(cascade = [CascadeType.ALL], orphanRemoval = true)
|
||||
val metadata: Map<String, FieldMetadata>
|
||||
val metadata: Map<String, FieldMetadata> = emptyMap()
|
||||
)
|
||||
+17
-15
@@ -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<String>,
|
||||
val publishedBy: Set<String>,
|
||||
val genres: Set<Genre>,
|
||||
val themes: Set<Theme>,
|
||||
val keywords: Set<String>,
|
||||
val screenshotUrls: Set<URL>,
|
||||
val videoUrls: Set<URL>,
|
||||
val features: Set<GameFeature>,
|
||||
val perspectives: Set<PlayerPerspective>
|
||||
val description: String? = null,
|
||||
val coverUrl: URI? = null,
|
||||
val release: Instant? = null,
|
||||
val userRating: Int? = null,
|
||||
val criticRating: Int? = null,
|
||||
val developedBy: Set<String>? = null,
|
||||
val publishedBy: Set<String>? = null,
|
||||
val genres: Set<Genre>? = null,
|
||||
val themes: Set<Theme>? = null,
|
||||
val keywords: Set<String>? = null,
|
||||
val screenshotUrls: Set<URI>? = null,
|
||||
val videoUrls: Set<URI>? = null,
|
||||
val features: Set<GameFeature>? = null,
|
||||
val perspectives: Set<PlayerPerspective>? = 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,
|
||||
|
||||
@@ -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<PluginConfigElement> = emptyList()
|
||||
|
||||
override fun validateConfig(config: Map<String, String?>): 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<SteamGame> = runBlocking { searchStore(gameId) }
|
||||
if (searchResult.isEmpty()) return null
|
||||
@@ -91,9 +88,11 @@ class SteamPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
|
||||
val steamDetailsResultWrapper: Map<Int, SteamDetailsResultWrapper> = 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<String> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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<Int>?,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user