mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-13 16:40:01 +00:00
Finish implementing IGDB plugin
Updated Plugin API Updated Steam plugin Other minor improvements
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
</option>
|
</option>
|
||||||
<option name="taskNames">
|
<option name="taskNames">
|
||||||
<list>
|
<list>
|
||||||
|
<option value="clean" />
|
||||||
<option value="build" />
|
<option value="build" />
|
||||||
</list>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
</option>
|
</option>
|
||||||
<option name="taskNames">
|
<option name="taskNames">
|
||||||
<list>
|
<list>
|
||||||
|
<option value="clean" />
|
||||||
<option value="build" />
|
<option value="build" />
|
||||||
</list>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
|
|||||||
+2
-6
@@ -94,19 +94,15 @@ class GameyfinPluginManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
log.info { "${"Start plugin '{}'"} ${getPluginLabel(pluginWrapper.descriptor)}"}
|
log.info { "Start plugin '${getPluginLabel(pluginWrapper.descriptor)}'"}
|
||||||
pluginWrapper.plugin.start()
|
pluginWrapper.plugin.start()
|
||||||
pluginWrapper.pluginState = PluginState.STARTED
|
pluginWrapper.pluginState = PluginState.STARTED
|
||||||
pluginWrapper.failedException = null
|
pluginWrapper.failedException = null
|
||||||
startedPlugins.add(pluginWrapper)
|
startedPlugins.add(pluginWrapper)
|
||||||
} catch (e: LinkageError) {
|
|
||||||
pluginWrapper.pluginState = PluginState.FAILED
|
|
||||||
pluginWrapper.failedException = e
|
|
||||||
log.error { "${"Unable to start plugin '{}'"} ${getPluginLabel(pluginWrapper.descriptor)} $e"}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
pluginWrapper.pluginState = PluginState.FAILED
|
pluginWrapper.pluginState = PluginState.FAILED
|
||||||
pluginWrapper.failedException = e
|
pluginWrapper.failedException = e
|
||||||
log.error { "${"Unable to start plugin '{}'"} ${getPluginLabel(pluginWrapper.descriptor)} $e"}
|
log.error { "Unable to start plugin '${getPluginLabel(pluginWrapper.descriptor)}': $e"}
|
||||||
} finally {
|
} finally {
|
||||||
firePluginStateEvent(PluginStateEvent(this, pluginWrapper, pluginState))
|
firePluginStateEvent(PluginStateEvent(this, pluginWrapper, pluginState))
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -9,9 +9,9 @@ import jakarta.annotation.security.RolesAllowed
|
|||||||
class PluginManagementEndpoint(
|
class PluginManagementEndpoint(
|
||||||
private val pluginManagementService: PluginManagementService
|
private val pluginManagementService: PluginManagementService
|
||||||
) {
|
) {
|
||||||
fun getPlugins() = pluginManagementService.getPlugins()
|
fun getPlugins() = pluginManagementService.getPluginDtos()
|
||||||
|
|
||||||
fun getPlugin(pluginId: String) = pluginManagementService.getPlugin(pluginId)
|
fun getPlugin(pluginId: String) = pluginManagementService.getPluginDto(pluginId)
|
||||||
|
|
||||||
fun startPlugin(pluginId: String) = pluginManagementService.startPlugin(pluginId)
|
fun startPlugin(pluginId: String) = pluginManagementService.startPlugin(pluginId)
|
||||||
|
|
||||||
|
|||||||
+17
-3
@@ -1,12 +1,15 @@
|
|||||||
package de.grimsi.gameyfin.core.plugins.management
|
package de.grimsi.gameyfin.core.plugins.management
|
||||||
|
|
||||||
|
import org.pf4j.ExtensionPoint
|
||||||
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class PluginManagementService(
|
class PluginManagementService(
|
||||||
private val pluginManager: GameyfinPluginManager
|
private val pluginManager: GameyfinPluginManager,
|
||||||
|
private val pluginManagementRepository: PluginManagementRepository
|
||||||
) {
|
) {
|
||||||
fun getPlugins(): List<PluginDto> {
|
fun getPluginDtos(): List<PluginDto> {
|
||||||
return pluginManager.plugins.map {
|
return pluginManager.plugins.map {
|
||||||
PluginDto(
|
PluginDto(
|
||||||
it.pluginId,
|
it.pluginId,
|
||||||
@@ -18,7 +21,7 @@ class PluginManagementService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPlugin(pluginId: String): PluginDto {
|
fun getPluginDto(pluginId: String): PluginDto {
|
||||||
val plugin = pluginManager.getPlugin(pluginId)
|
val plugin = pluginManager.getPlugin(pluginId)
|
||||||
return PluginDto(
|
return PluginDto(
|
||||||
plugin.pluginId,
|
plugin.pluginId,
|
||||||
@@ -29,6 +32,17 @@ class PluginManagementService(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getPluginManagementEntry(pluginId: String): PluginManagementEntry {
|
||||||
|
return pluginManagementRepository.findByIdOrNull(pluginId)
|
||||||
|
?: throw IllegalArgumentException("Plugin with ID $pluginId not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPluginManagementEntry(clazz: Class<ExtensionPoint>): PluginManagementEntry {
|
||||||
|
val pluginWrapper = pluginManager.whichPlugin(clazz)
|
||||||
|
return pluginManagementRepository.findByIdOrNull(pluginWrapper.pluginId)
|
||||||
|
?: throw IllegalArgumentException("Plugin with class $clazz not found")
|
||||||
|
}
|
||||||
|
|
||||||
fun startPlugin(pluginId: String) {
|
fun startPlugin(pluginId: String) {
|
||||||
pluginManager.startPlugin(pluginId)
|
pluginManager.startPlugin(pluginId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
package de.grimsi.gameyfin.games
|
|
||||||
|
|
||||||
import jakarta.persistence.*
|
|
||||||
import java.time.Instant
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
class Game(
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
|
||||||
var id: Long? = null,
|
|
||||||
|
|
||||||
val title: String,
|
|
||||||
|
|
||||||
@Lob
|
|
||||||
@Column(columnDefinition = "CLOB")
|
|
||||||
val comment: String? = null,
|
|
||||||
|
|
||||||
@Lob
|
|
||||||
@Column(columnDefinition = "CLOB")
|
|
||||||
val summary: String,
|
|
||||||
|
|
||||||
val release: Instant,
|
|
||||||
|
|
||||||
@ElementCollection
|
|
||||||
val publishers: List<String>,
|
|
||||||
|
|
||||||
@ElementCollection
|
|
||||||
val developers: List<String>,
|
|
||||||
|
|
||||||
@Column(unique = true)
|
|
||||||
val path: String,
|
|
||||||
|
|
||||||
val source: String
|
|
||||||
)
|
|
||||||
@@ -1,5 +1,14 @@
|
|||||||
package de.grimsi.gameyfin.games
|
package de.grimsi.gameyfin.games
|
||||||
|
|
||||||
|
import de.grimsi.gameyfin.core.plugins.management.PluginManagementService
|
||||||
|
import de.grimsi.gameyfin.games.entities.Company
|
||||||
|
import de.grimsi.gameyfin.games.entities.CompanyType
|
||||||
|
import de.grimsi.gameyfin.games.entities.Game
|
||||||
|
import de.grimsi.gameyfin.games.entities.Screenshot
|
||||||
|
import de.grimsi.gameyfin.games.repositories.CompanyRepository
|
||||||
|
import de.grimsi.gameyfin.games.repositories.GameRepository
|
||||||
|
import de.grimsi.gameyfin.games.repositories.ScreenshotContentStore
|
||||||
|
import de.grimsi.gameyfin.games.repositories.ScreenshotRepository
|
||||||
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadata
|
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadata
|
||||||
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadataProvider
|
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadataProvider
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
@@ -9,12 +18,18 @@ import kotlinx.coroutines.runBlocking
|
|||||||
import org.pf4j.PluginManager
|
import org.pf4j.PluginManager
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
import java.net.URL
|
||||||
|
import java.net.URLConnection
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class GameService(
|
class GameService(
|
||||||
|
private val pluginManager: PluginManager,
|
||||||
|
private val pluginManagementService: PluginManagementService,
|
||||||
private val gameRepository: GameRepository,
|
private val gameRepository: GameRepository,
|
||||||
private val pluginManager: PluginManager
|
private val companyRepository: CompanyRepository,
|
||||||
|
private val screenshotRepository: ScreenshotRepository,
|
||||||
|
private val screenshotContentStore: ScreenshotContentStore
|
||||||
) {
|
) {
|
||||||
private val log = KotlinLogging.logger {}
|
private val log = KotlinLogging.logger {}
|
||||||
|
|
||||||
@@ -47,15 +62,11 @@ class GameService(
|
|||||||
val (plugin, metadata) = metadataResults.entries.firstOrNull { it.value != null }
|
val (plugin, metadata) = metadataResults.entries.firstOrNull { it.value != null }
|
||||||
?: throw NoMatchException("Could not match game at $path")
|
?: throw NoMatchException("Could not match game at $path")
|
||||||
|
|
||||||
val game = Game(
|
if (metadata == null) {
|
||||||
title = metadata!!.title,
|
throw NoMatchException("Plugin ${plugin.javaClass} returned invalid metadata for game at $path")
|
||||||
summary = metadata.description,
|
}
|
||||||
release = metadata.release,
|
|
||||||
publishers = metadata.publishedBy,
|
val game = toEntity(metadata, path, plugin)
|
||||||
developers = metadata.developedBy,
|
|
||||||
path = path.toString(),
|
|
||||||
source = plugin.javaClass.name
|
|
||||||
)
|
|
||||||
|
|
||||||
return createOrUpdate(game)
|
return createOrUpdate(game)
|
||||||
}
|
}
|
||||||
@@ -74,13 +85,48 @@ class GameService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun toDto(game: Game): GameDto {
|
private fun toDto(game: Game): GameDto {
|
||||||
if (game.id == null) {
|
val gameId = game.id ?: throw IllegalArgumentException("Game ID is null")
|
||||||
throw IllegalArgumentException("Game ID is null")
|
|
||||||
}
|
|
||||||
|
|
||||||
return GameDto(
|
return GameDto(
|
||||||
id = game.id!!,
|
id = gameId,
|
||||||
title = game.title
|
title = game.title
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun toEntity(metadata: GameMetadata, path: Path, source: GameMetadataProvider): Game {
|
||||||
|
return Game(
|
||||||
|
title = metadata.title,
|
||||||
|
summary = metadata.description,
|
||||||
|
release = metadata.release,
|
||||||
|
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,
|
||||||
|
screenshots = metadata.screenshotUrls.map { downloadAndPersist(it) }.toSet(),
|
||||||
|
videoUrls = metadata.videoUrls,
|
||||||
|
path = path.toString(),
|
||||||
|
source = pluginManagementService.getPluginManagementEntry(source.javaClass)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toEntity(companyName: String, companyType: CompanyType): Company {
|
||||||
|
companyRepository.findByNameAndType(companyName, companyType)?.let { return it }
|
||||||
|
val company = Company(name = companyName, type = companyType)
|
||||||
|
return companyRepository.save(company)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun downloadAndPersist(screenshotUrl: URL): Screenshot {
|
||||||
|
screenshotRepository.findByOriginalUrl(screenshotUrl)?.let { return it }
|
||||||
|
|
||||||
|
val screenshot = Screenshot(originalUrl = screenshotUrl)
|
||||||
|
screenshotUrl.openStream().use { input ->
|
||||||
|
val mimeType = URLConnection.guessContentTypeFromStream(input)
|
||||||
|
screenshot.mimeType = mimeType
|
||||||
|
screenshotContentStore.setContent(screenshot, input)
|
||||||
|
}
|
||||||
|
return screenshotRepository.save(screenshot)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package de.grimsi.gameyfin.games.entities
|
||||||
|
|
||||||
|
import jakarta.persistence.*
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(uniqueConstraints = [UniqueConstraint(columnNames = ["name", "type"])])
|
||||||
|
class Company(
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||||
|
var id: Long? = null,
|
||||||
|
val name: String,
|
||||||
|
val type: CompanyType
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class CompanyType {
|
||||||
|
DEVELOPER,
|
||||||
|
PUBLISHER
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package de.grimsi.gameyfin.games.entities
|
||||||
|
|
||||||
|
import de.grimsi.gameyfin.core.plugins.management.PluginManagementEntry
|
||||||
|
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameFeature
|
||||||
|
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.time.Instant
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
class Game(
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||||
|
var id: Long? = null,
|
||||||
|
|
||||||
|
val title: String,
|
||||||
|
|
||||||
|
@Lob
|
||||||
|
@Column(columnDefinition = "CLOB")
|
||||||
|
val comment: String? = null,
|
||||||
|
|
||||||
|
@Lob
|
||||||
|
@Column(columnDefinition = "CLOB")
|
||||||
|
val summary: String,
|
||||||
|
|
||||||
|
val release: Instant,
|
||||||
|
|
||||||
|
@OneToMany(cascade = [CascadeType.MERGE])
|
||||||
|
val publishers: Set<Company>,
|
||||||
|
|
||||||
|
@OneToMany(cascade = [CascadeType.MERGE])
|
||||||
|
val developers: Set<Company>,
|
||||||
|
|
||||||
|
@ElementCollection
|
||||||
|
val genres: Set<Genre>,
|
||||||
|
|
||||||
|
@ElementCollection
|
||||||
|
val themes: Set<Theme>,
|
||||||
|
|
||||||
|
@ElementCollection
|
||||||
|
val keywords: Set<String>,
|
||||||
|
|
||||||
|
@ElementCollection
|
||||||
|
val features: Set<GameFeature>,
|
||||||
|
|
||||||
|
@ElementCollection
|
||||||
|
val perspectives: Set<PlayerPerspective>,
|
||||||
|
|
||||||
|
@OneToMany(cascade = [CascadeType.MERGE])
|
||||||
|
val screenshots: Set<Screenshot>,
|
||||||
|
|
||||||
|
@ElementCollection
|
||||||
|
val videoUrls: Set<URL>,
|
||||||
|
|
||||||
|
@Column(unique = true)
|
||||||
|
val path: String,
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
val source: PluginManagementEntry
|
||||||
|
)
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package de.grimsi.gameyfin.games.entities
|
||||||
|
|
||||||
|
import jakarta.annotation.Nullable
|
||||||
|
import jakarta.persistence.Entity
|
||||||
|
import jakarta.persistence.GeneratedValue
|
||||||
|
import jakarta.persistence.GenerationType
|
||||||
|
import jakarta.persistence.Id
|
||||||
|
import org.springframework.content.commons.annotations.ContentId
|
||||||
|
import org.springframework.content.commons.annotations.ContentLength
|
||||||
|
import org.springframework.content.commons.annotations.MimeType
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
class Screenshot(
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||||
|
var id: Long? = null,
|
||||||
|
|
||||||
|
val originalUrl: URL,
|
||||||
|
|
||||||
|
@ContentId
|
||||||
|
@Nullable
|
||||||
|
var contentId: String? = null,
|
||||||
|
|
||||||
|
@ContentLength
|
||||||
|
@Nullable
|
||||||
|
var contentLength: Long? = null,
|
||||||
|
|
||||||
|
@MimeType
|
||||||
|
@Nullable
|
||||||
|
var mimeType: String? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package de.grimsi.gameyfin.games.entities
|
||||||
|
|
||||||
|
class Video {
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package de.grimsi.gameyfin.games.repositories
|
||||||
|
|
||||||
|
import de.grimsi.gameyfin.games.entities.Company
|
||||||
|
import de.grimsi.gameyfin.games.entities.CompanyType
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
|
||||||
|
interface CompanyRepository : JpaRepository<Company, Long> {
|
||||||
|
fun findByNameAndType(name: String, type: CompanyType): Company?
|
||||||
|
}
|
||||||
+2
-1
@@ -1,5 +1,6 @@
|
|||||||
package de.grimsi.gameyfin.games
|
package de.grimsi.gameyfin.games.repositories
|
||||||
|
|
||||||
|
import de.grimsi.gameyfin.games.entities.Game
|
||||||
import org.springframework.data.jpa.repository.JpaRepository
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
|
||||||
interface GameRepository : JpaRepository<Game, Long> {
|
interface GameRepository : JpaRepository<Game, Long> {
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
package de.grimsi.gameyfin.games.repositories
|
||||||
|
|
||||||
|
import de.grimsi.gameyfin.games.entities.Screenshot
|
||||||
|
import org.springframework.content.commons.store.ContentStore
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
interface ScreenshotContentStore : ContentStore<Screenshot, String>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package de.grimsi.gameyfin.games.repositories
|
||||||
|
|
||||||
|
import de.grimsi.gameyfin.games.entities.Screenshot
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
interface ScreenshotRepository : JpaRepository<Screenshot, Long> {
|
||||||
|
fun findByOriginalUrl(originalUrl: URL): Screenshot?
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package de.grimsi.gameyfin.libraries
|
package de.grimsi.gameyfin.libraries
|
||||||
|
|
||||||
import de.grimsi.gameyfin.games.Game
|
import de.grimsi.gameyfin.games.entities.Game
|
||||||
import jakarta.persistence.*
|
import jakarta.persistence.*
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package de.grimsi.gameyfin.libraries
|
|||||||
|
|
||||||
import com.vaadin.hilla.Endpoint
|
import com.vaadin.hilla.Endpoint
|
||||||
import de.grimsi.gameyfin.core.Role
|
import de.grimsi.gameyfin.core.Role
|
||||||
import de.grimsi.gameyfin.games.Game
|
import de.grimsi.gameyfin.games.entities.Game
|
||||||
import jakarta.annotation.security.RolesAllowed
|
import jakarta.annotation.security.RolesAllowed
|
||||||
|
|
||||||
@Endpoint
|
@Endpoint
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package de.grimsi.gameyfin.libraries
|
|||||||
|
|
||||||
import de.grimsi.gameyfin.config.ConfigProperties
|
import de.grimsi.gameyfin.config.ConfigProperties
|
||||||
import de.grimsi.gameyfin.config.ConfigService
|
import de.grimsi.gameyfin.config.ConfigService
|
||||||
import de.grimsi.gameyfin.games.Game
|
|
||||||
import de.grimsi.gameyfin.games.GameService
|
import de.grimsi.gameyfin.games.GameService
|
||||||
|
import de.grimsi.gameyfin.games.entities.Game
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
|||||||
+12
-9
@@ -9,14 +9,15 @@ class GameMetadata(
|
|||||||
val release: Instant,
|
val release: Instant,
|
||||||
val userRating: Int?,
|
val userRating: Int?,
|
||||||
val criticRating: Int?,
|
val criticRating: Int?,
|
||||||
val developedBy: List<String>,
|
val developedBy: Set<String>,
|
||||||
val publishedBy: List<String>,
|
val publishedBy: Set<String>,
|
||||||
val genres: List<Genre>,
|
val genres: Set<Genre>,
|
||||||
val themes: List<Theme>,
|
val themes: Set<Theme>,
|
||||||
val screenshotUrls: List<URL>,
|
val keywords: Set<String>,
|
||||||
val videoUrls: List<URL>,
|
val screenshotUrls: Set<URL>,
|
||||||
val features: List<GameFeature>,
|
val videoUrls: Set<URL>,
|
||||||
val perspectives: List<PlayerPerspective>
|
val features: Set<GameFeature>,
|
||||||
|
val perspectives: Set<PlayerPerspective>
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class Genre {
|
enum class Genre {
|
||||||
@@ -95,10 +96,12 @@ enum class GameFeature {
|
|||||||
ONLINE_PVE,
|
ONLINE_PVE,
|
||||||
LOCAL_PVP,
|
LOCAL_PVP,
|
||||||
LOCAL_PVE,
|
LOCAL_PVE,
|
||||||
CROSSPLAY
|
CROSSPLAY,
|
||||||
|
SPLITSCREEN
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class PlayerPerspective {
|
enum class PlayerPerspective {
|
||||||
|
UNKNOWN,
|
||||||
FIRST_PERSON,
|
FIRST_PERSON,
|
||||||
THIRD_PERSON,
|
THIRD_PERSON,
|
||||||
BIRD_VIEW_ISOMETRIC,
|
BIRD_VIEW_ISOMETRIC,
|
||||||
|
|||||||
@@ -59,9 +59,43 @@ class IgdbPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
|
|||||||
|
|
||||||
@Extension
|
@Extension
|
||||||
class IgdbMetadataProvider : GameMetadataProvider {
|
class IgdbMetadataProvider : GameMetadataProvider {
|
||||||
|
|
||||||
|
private val QUERY_FIELDS = listOf(
|
||||||
|
"slug",
|
||||||
|
"name",
|
||||||
|
"summary",
|
||||||
|
"first_release_date",
|
||||||
|
"rating",
|
||||||
|
"aggregated_rating",
|
||||||
|
"total_rating",
|
||||||
|
"category",
|
||||||
|
"multiplayer_modes.lancoop",
|
||||||
|
"game_modes.slug",
|
||||||
|
"game_modes.name",
|
||||||
|
"cover.image_id",
|
||||||
|
"screenshots.image_id",
|
||||||
|
"videos.video_id",
|
||||||
|
"involved_companies.company.slug",
|
||||||
|
"involved_companies.company.name",
|
||||||
|
"involved_companies.developer",
|
||||||
|
"involved_companies.publisher",
|
||||||
|
"involved_companies.company.logo.image_id",
|
||||||
|
"genres.slug",
|
||||||
|
"genres.name",
|
||||||
|
"keywords.slug",
|
||||||
|
"keywords.name",
|
||||||
|
"themes.slug",
|
||||||
|
"themes.name",
|
||||||
|
"player_perspectives.slug",
|
||||||
|
"player_perspectives.name",
|
||||||
|
"platforms.slug",
|
||||||
|
"platforms.name",
|
||||||
|
"platforms.platform_logo.image_id"
|
||||||
|
).joinToString(",")
|
||||||
|
|
||||||
override fun fetchMetadata(gameId: String): GameMetadata? {
|
override fun fetchMetadata(gameId: String): GameMetadata? {
|
||||||
val findBySlugQuery = APICalypse()
|
val findBySlugQuery = APICalypse()
|
||||||
.fields("*")
|
.fields(QUERY_FIELDS)
|
||||||
.where("slug = \"${guessSlug(gameId)}\"")
|
.where("slug = \"${guessSlug(gameId)}\"")
|
||||||
|
|
||||||
// First step: Try to find the game by guessing the slug
|
// First step: Try to find the game by guessing the slug
|
||||||
@@ -70,7 +104,7 @@ class IgdbPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
|
|||||||
// Second step: Try a fuzzy search
|
// Second step: Try a fuzzy search
|
||||||
if (game == null) {
|
if (game == null) {
|
||||||
val searchByNameQuery = APICalypse()
|
val searchByNameQuery = APICalypse()
|
||||||
.fields("*")
|
.fields(QUERY_FIELDS)
|
||||||
.limit(100)
|
.limit(100)
|
||||||
.search(gameId)
|
.search(gameId)
|
||||||
|
|
||||||
@@ -91,14 +125,15 @@ class IgdbPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
|
|||||||
release = Instant.ofEpochSecond(game.firstReleaseDate.seconds),
|
release = Instant.ofEpochSecond(game.firstReleaseDate.seconds),
|
||||||
userRating = game.rating.toInt(),
|
userRating = game.rating.toInt(),
|
||||||
criticRating = game.aggregatedRating.toInt(),
|
criticRating = game.aggregatedRating.toInt(),
|
||||||
developedBy = game.involvedCompaniesList.filter { it.developer }.map { it.company.name },
|
developedBy = game.involvedCompaniesList.filter { it.developer }.map { it.company.name }.toSet(),
|
||||||
publishedBy = game.involvedCompaniesList.filter { it.publisher }.map { it.company.name },
|
publishedBy = game.involvedCompaniesList.filter { it.publisher }.map { it.company.name }.toSet(),
|
||||||
genres = game.genresList.map { Mapper.genre(it) },
|
genres = game.genresList.map { Mapper.genre(it) }.toSet(),
|
||||||
themes = game.themesList.map { Mapper.theme(it) },
|
themes = game.themesList.map { Mapper.theme(it) }.toSet(),
|
||||||
screenshotUrls = listOf(),
|
keywords = game.keywordsList.map { it.name }.toSet(),
|
||||||
videoUrls = listOf(),
|
screenshotUrls = game.screenshotsList.map { Mapper.screenshot(it) }.toSet(),
|
||||||
features = listOf(),
|
videoUrls = game.videosList.map { Mapper.video(it) }.toSet(),
|
||||||
perspectives = listOf()
|
features = Mapper.gameFeatures(game),
|
||||||
|
perspectives = game.playerPerspectivesList.map { Mapper.playerPerspective(it) }.toSet()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
package de.grimsi.gameyfin.plugins.igdb
|
package de.grimsi.gameyfin.plugins.igdb
|
||||||
|
|
||||||
|
import com.api.igdb.utils.ImageSize
|
||||||
|
import com.api.igdb.utils.ImageType
|
||||||
|
import com.api.igdb.utils.imageBuilder
|
||||||
|
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameFeature
|
||||||
import de.grimsi.gameyfin.pluginapi.gamemetadata.Genre
|
import de.grimsi.gameyfin.pluginapi.gamemetadata.Genre
|
||||||
|
import de.grimsi.gameyfin.pluginapi.gamemetadata.PlayerPerspective
|
||||||
import de.grimsi.gameyfin.pluginapi.gamemetadata.Theme
|
import de.grimsi.gameyfin.pluginapi.gamemetadata.Theme
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.net.URI
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
class Mapper {
|
class Mapper {
|
||||||
companion object {
|
companion object {
|
||||||
@@ -70,5 +77,49 @@ class Mapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun playerPerspective(perspective: proto.PlayerPerspective): PlayerPerspective {
|
||||||
|
return when (perspective.slug) {
|
||||||
|
"first-person" -> PlayerPerspective.FIRST_PERSON
|
||||||
|
"third-person" -> PlayerPerspective.THIRD_PERSON
|
||||||
|
"bird-view-isometric" -> PlayerPerspective.BIRD_VIEW_ISOMETRIC
|
||||||
|
"side-view" -> PlayerPerspective.SIDE_VIEW
|
||||||
|
"text" -> PlayerPerspective.TEXT
|
||||||
|
"auditory" -> PlayerPerspective.AUDITORY
|
||||||
|
"virtual-reality" -> PlayerPerspective.VIRTUAL_REALITY
|
||||||
|
else -> {
|
||||||
|
log.warn("Unknown player perspective: {}", perspective.slug)
|
||||||
|
PlayerPerspective.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun screenshot(screenshot: proto.Screenshot): URL {
|
||||||
|
return URI(imageBuilder(screenshot.imageId, ImageSize.SCREENSHOT_HUGE, ImageType.PNG)).toURL()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun video(video: proto.GameVideo): URL {
|
||||||
|
return URI("https://www.youtube.com/watch?v=${video.videoId}").toURL()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun gameFeatures(game: proto.Game): Set<GameFeature> {
|
||||||
|
var gameFeatures = mutableSetOf<GameFeature>()
|
||||||
|
|
||||||
|
// Get LAN support from multiplayer modes
|
||||||
|
if (game.multiplayerModesList.any { it.lancoop }) gameFeatures.add(GameFeature.LOCAL_MULTIPLAYER)
|
||||||
|
|
||||||
|
for (gameMode in game.gameModesList) {
|
||||||
|
when (gameMode.slug) {
|
||||||
|
"single-player" -> gameFeatures.add(GameFeature.SINGLEPLAYER)
|
||||||
|
"multiplayer" -> gameFeatures.add(GameFeature.MULTIPLAYER)
|
||||||
|
"co-operative" -> gameFeatures.add(GameFeature.CO_OP)
|
||||||
|
"split-screen" -> gameFeatures.add(GameFeature.SPLITSCREEN)
|
||||||
|
else -> {
|
||||||
|
log.warn("Unknown game mode: {}", gameMode.slug)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return gameFeatures
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,14 +112,15 @@ class SteamPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
|
|||||||
release = date(game["release_date"]?.jsonObject["date"]?.jsonPrimitive?.content!!),
|
release = date(game["release_date"]?.jsonObject["date"]?.jsonPrimitive?.content!!),
|
||||||
userRating = 0,
|
userRating = 0,
|
||||||
criticRating = 0,
|
criticRating = 0,
|
||||||
developedBy = stringList(game, "developers"),
|
developedBy = stringList(game, "developers").toSet(),
|
||||||
publishedBy = stringList(game, "publishers"),
|
publishedBy = stringList(game, "publishers").toSet(),
|
||||||
genres = emptyList(),
|
genres = emptySet(),
|
||||||
themes = emptyList(),
|
themes = emptySet(),
|
||||||
screenshotUrls = emptyList(),
|
keywords = emptySet(),
|
||||||
videoUrls = emptyList(),
|
screenshotUrls = emptySet(),
|
||||||
features = emptyList(),
|
videoUrls = emptySet(),
|
||||||
perspectives = emptyList()
|
features = emptySet(),
|
||||||
|
perspectives = emptySet()
|
||||||
)
|
)
|
||||||
|
|
||||||
return metadata
|
return metadata
|
||||||
|
|||||||
Reference in New Issue
Block a user