mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-16 08:15:48 +00:00
Merge branch 'main' into feature/643-request-system
# Conflicts: # app/src/main/kotlin/org/gameyfin/app/libraries/LibraryScanService.kt
This commit is contained in:
@@ -19,7 +19,15 @@ runs:
|
|||||||
username: ${{ inputs.ghcr_username }}
|
username: ${{ inputs.ghcr_username }}
|
||||||
password: ${{ inputs.ghcr_token }}
|
password: ${{ inputs.ghcr_token }}
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Prepare Ubuntu tags
|
||||||
|
id: ubuntu_tags
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
TAGS="${{ inputs.tags }}"
|
||||||
|
UBUNTU_TAGS=$(echo "$TAGS" | awk -F, '{for(i=1;i<=NF;i++){split($i,a,":"); printf "%s:%s-ubuntu", a[1], a[2]; if(i<NF) printf ","}}')
|
||||||
|
echo "ubuntu_tags=$UBUNTU_TAGS" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Build and push Docker image (Alpine)
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: ${{ inputs.context }}
|
context: ${{ inputs.context }}
|
||||||
@@ -30,6 +38,17 @@ runs:
|
|||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha
|
cache-to: type=gha
|
||||||
|
|
||||||
|
- name: Build and push Docker image (Ubuntu)
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: ${{ inputs.context }}
|
||||||
|
file: docker/Dockerfile.ubuntu
|
||||||
|
platforms: ${{ inputs.platforms }}
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.ubuntu_tags.outputs.ubuntu_tags }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha
|
||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
dockerhub_username:
|
dockerhub_username:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ jobs:
|
|||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ jobs:
|
|||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
release_version: ${{ steps.get_version.outputs.release_version }}
|
release_version: ${{ steps.get_version.outputs.release_version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -64,17 +64,17 @@ jobs:
|
|||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Download modified files
|
- name: Download modified files
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: modified-files
|
name: modified-files
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
@@ -119,17 +119,17 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Download modified files
|
- name: Download modified files
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: modified-files
|
name: modified-files
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
@@ -150,12 +150,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Download modified files
|
- name: Download modified files
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: modified-files
|
name: modified-files
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "gameyfin",
|
"name": "gameyfin",
|
||||||
"version": "2.0.0",
|
"version": "2.0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@heroui/react": "2.7.9",
|
"@heroui/react": "2.7.9",
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ function SsoManagementLayout({getConfig, formik, setSaveMessage}: any) {
|
|||||||
<ConfigFormField configElement={getConfig("sso.oidc.enabled")}/>
|
<ConfigFormField configElement={getConfig("sso.oidc.enabled")}/>
|
||||||
|
|
||||||
<Section title="SSO user handling"/>
|
<Section title="SSO user handling"/>
|
||||||
<div className="flex flex-row items-baseline">
|
<div className="flex flex-row items-baseline mb-4">
|
||||||
<CheckboxGroup className="flex flex-col flex-1 items-baseline gap-2"
|
<CheckboxGroup className="flex flex-col flex-1 items-baseline gap-2"
|
||||||
value={["auto-register-new-users"]}>
|
value={["auto-register-new-users"]}>
|
||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
@@ -70,6 +70,13 @@ function SsoManagementLayout({getConfig, formik, setSaveMessage}: any) {
|
|||||||
!formik.values.sso.oidc["auto-register-new-users"]}/>
|
!formik.values.sso.oidc["auto-register-new-users"]}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row items-center gap-4">
|
||||||
|
<ConfigFormField configElement={getConfig("sso.oidc.roles-claim")}
|
||||||
|
isDisabled={!formik.values.sso.oidc.enabled}/>
|
||||||
|
<ConfigFormField configElement={getConfig("sso.oidc.oauth-scopes")}
|
||||||
|
isDisabled={!formik.values.sso.oidc.enabled}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Section title="SSO provider configuration"/>
|
<Section title="SSO provider configuration"/>
|
||||||
<ConfigFormField configElement={getConfig("sso.oidc.client-id")}
|
<ConfigFormField configElement={getConfig("sso.oidc.client-id")}
|
||||||
isDisabled={!formik.values.sso.oidc.enabled}/>
|
isDisabled={!formik.values.sso.oidc.enabled}/>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {Card, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, useDisclosure} from "@heroui/react";
|
import {Button, Card, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, useDisclosure} from "@heroui/react";
|
||||||
import {DotsThreeVertical} from "@phosphor-icons/react";
|
import {DotsThreeVertical} from "@phosphor-icons/react";
|
||||||
import {useEffect, useState} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import {MessageEndpoint, PasswordResetEndpoint, UserEndpoint} from "Frontend/generated/endpoints";
|
import {MessageEndpoint, PasswordResetEndpoint, UserEndpoint} from "Frontend/generated/endpoints";
|
||||||
import {AvatarEndpoint} from "Frontend/endpoints/endpoints";
|
import {AvatarEndpoint} from "Frontend/endpoints/endpoints";
|
||||||
import Avatar from "Frontend/components/general/Avatar";
|
import Avatar from "Frontend/components/general/Avatar";
|
||||||
@@ -108,6 +108,27 @@ export function UserManagementCard({user}: { user: UserInfoDto }) {
|
|||||||
<>
|
<>
|
||||||
<Card
|
<Card
|
||||||
className={`flex flex-row justify-between p-2 ${userEnabled ? "" : "bg-warning/25"} ${user.managedBySso ? "text-foreground/50" : ""}`}>
|
className={`flex flex-row justify-between p-2 ${userEnabled ? "" : "bg-warning/25"} ${user.managedBySso ? "text-foreground/50" : ""}`}>
|
||||||
|
<div className="absolute right-0 top-0">
|
||||||
|
<Dropdown placement="bottom-end" size="sm" backdrop="opaque">
|
||||||
|
<DropdownTrigger>
|
||||||
|
<Button isIconOnly variant="light">
|
||||||
|
<DotsThreeVertical/>
|
||||||
|
</Button>
|
||||||
|
</DropdownTrigger>
|
||||||
|
<DropdownMenu aria-label="Static Actions" items={dropdownItems} disabledKeys={disabledKeys}>
|
||||||
|
{(item) => (
|
||||||
|
<DropdownItem
|
||||||
|
key={item.key}
|
||||||
|
onPress={item.onPress}
|
||||||
|
color={item.key === "delete" ? "danger" : "default"}
|
||||||
|
className={item.key === "delete" ? "text-danger" : ""}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</DropdownItem>
|
||||||
|
)}
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
<div className="flex flex-row items-center gap-4">
|
<div className="flex flex-row items-center gap-4">
|
||||||
<Avatar username={user.username}
|
<Avatar username={user.username}
|
||||||
name={user.username?.charAt(0)}
|
name={user.username?.charAt(0)}
|
||||||
@@ -118,30 +139,12 @@ export function UserManagementCard({user}: { user: UserInfoDto }) {
|
|||||||
}}/>
|
}}/>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<p className="font-semibold">{user.username}</p>
|
<p className="font-semibold">{user.username}</p>
|
||||||
<p className="text-sm">{user.email}</p>
|
<p className="text-sm max-w-44 truncate" title={user.email}>{user.email}</p>
|
||||||
{user.roles?.map((role) => (
|
{user.roles?.map((role) => (
|
||||||
<RoleChip role={role as string}/>
|
<RoleChip role={role as string}/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dropdown placement="bottom-end" size="sm" backdrop="opaque">
|
|
||||||
<DropdownTrigger>
|
|
||||||
<DotsThreeVertical cursor="pointer"/>
|
|
||||||
</DropdownTrigger>
|
|
||||||
<DropdownMenu aria-label="Static Actions" items={dropdownItems} disabledKeys={disabledKeys}>
|
|
||||||
{(item) => (
|
|
||||||
<DropdownItem
|
|
||||||
key={item.key}
|
|
||||||
onPress={item.onPress}
|
|
||||||
color={item.key === "delete" ? "danger" : "default"}
|
|
||||||
className={item.key === "delete" ? "text-danger" : ""}
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</DropdownItem>
|
|
||||||
)}
|
|
||||||
</DropdownMenu>
|
|
||||||
</Dropdown>
|
|
||||||
</Card>
|
</Card>
|
||||||
<ConfirmUserDeletionModal isOpen={userDeletionConfirmationModal.isOpen}
|
<ConfirmUserDeletionModal isOpen={userDeletionConfirmationModal.isOpen}
|
||||||
onOpenChange={userDeletionConfirmationModal.onOpenChange}
|
onOpenChange={userDeletionConfirmationModal.onOpenChange}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export default function GameView() {
|
|||||||
}, {} as Record<string, ComboButtonOption>);
|
}, {} as Record<string, ComboButtonOption>);
|
||||||
setDownloadOptions(options);
|
setDownloadOptions(options);
|
||||||
});
|
});
|
||||||
}, []);
|
}, [gameId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state.isLoaded && (!gameId || !state.state[parseInt(gameId)])) {
|
if (state.isLoaded && (!gameId || !state.state[parseInt(gameId)])) {
|
||||||
|
|||||||
@@ -147,6 +147,20 @@ sealed class ConfigProperties<T : Serializable>(
|
|||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data object RolesClaim : ConfigProperties<String>(
|
||||||
|
String::class,
|
||||||
|
"sso.oidc.roles-claim",
|
||||||
|
"JWT claim to extract roles from",
|
||||||
|
"roles"
|
||||||
|
)
|
||||||
|
|
||||||
|
data object OAuthScopes : ConfigProperties<Array<String>>(
|
||||||
|
Array<String>::class,
|
||||||
|
"sso.oidc.oauth-scopes",
|
||||||
|
"OAuth2 scopes to request",
|
||||||
|
arrayOf("openid", "profile", "email", "roles")
|
||||||
|
)
|
||||||
|
|
||||||
data object ClientId : ConfigProperties<String>(
|
data object ClientId : ConfigProperties<String>(
|
||||||
String::class,
|
String::class,
|
||||||
"sso.oidc.client-id",
|
"sso.oidc.client-id",
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ class SecurityConfig(
|
|||||||
val clientRegistration = ClientRegistration.withRegistrationId(SSO_PROVIDER_KEY)
|
val clientRegistration = ClientRegistration.withRegistrationId(SSO_PROVIDER_KEY)
|
||||||
.clientId(config.get(ConfigProperties.SSO.OIDC.ClientId))
|
.clientId(config.get(ConfigProperties.SSO.OIDC.ClientId))
|
||||||
.clientSecret(config.get(ConfigProperties.SSO.OIDC.ClientSecret))
|
.clientSecret(config.get(ConfigProperties.SSO.OIDC.ClientSecret))
|
||||||
.scope("openid", "profile", "email")
|
.scope(config.get(ConfigProperties.SSO.OIDC.OAuthScopes)?.toList())
|
||||||
.userNameAttributeName("preferred_username")
|
.userNameAttributeName("preferred_username")
|
||||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||||
.issuerUri(config.get(ConfigProperties.SSO.OIDC.IssuerUrl))
|
.issuerUri(config.get(ConfigProperties.SSO.OIDC.IssuerUrl))
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
package org.gameyfin.app.games.entities
|
package org.gameyfin.app.games.entities
|
||||||
|
|
||||||
import jakarta.persistence.Entity
|
import jakarta.persistence.*
|
||||||
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.ContentId
|
||||||
import org.springframework.content.commons.annotations.ContentLength
|
import org.springframework.content.commons.annotations.ContentLength
|
||||||
import org.springframework.content.commons.annotations.MimeType
|
import org.springframework.content.commons.annotations.MimeType
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@EntityListeners(ImageEntityListener::class)
|
||||||
class Image(
|
class Image(
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package org.gameyfin.app.games.entities
|
||||||
|
|
||||||
|
import jakarta.persistence.PostRemove
|
||||||
|
import org.gameyfin.app.media.ImageService
|
||||||
|
import org.springframework.context.ApplicationContext
|
||||||
|
import org.springframework.context.ApplicationContextAware
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class ImageEntityListener : ApplicationContextAware {
|
||||||
|
companion object {
|
||||||
|
private lateinit var applicationContext: ApplicationContext
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setApplicationContext(context: ApplicationContext) {
|
||||||
|
applicationContext = context
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getImageService(): ImageService {
|
||||||
|
return applicationContext.getBean(ImageService::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostRemove
|
||||||
|
fun deleted(image: Image) {
|
||||||
|
getImageService().deleteFile(image)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,14 +4,17 @@ import io.github.oshai.kotlinlogging.KotlinLogging
|
|||||||
import org.gameyfin.app.core.filesystem.FilesystemService
|
import org.gameyfin.app.core.filesystem.FilesystemService
|
||||||
import org.gameyfin.app.games.GameService
|
import org.gameyfin.app.games.GameService
|
||||||
import org.gameyfin.app.games.entities.Game
|
import org.gameyfin.app.games.entities.Game
|
||||||
import org.gameyfin.app.libraries.dto.*
|
import org.gameyfin.app.games.entities.Image
|
||||||
import org.gameyfin.app.libraries.entities.Library
|
import org.gameyfin.app.libraries.dto.LibraryScanProgress
|
||||||
|
import org.gameyfin.app.libraries.dto.LibraryScanStatus
|
||||||
|
import org.gameyfin.app.libraries.dto.LibraryScanStep
|
||||||
import org.gameyfin.app.libraries.enums.ScanType
|
import org.gameyfin.app.libraries.enums.ScanType
|
||||||
import org.gameyfin.app.libraries.scan.*
|
import org.gameyfin.app.libraries.scan.*
|
||||||
import org.gameyfin.app.media.ImageService
|
import org.gameyfin.app.media.ImageService
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import reactor.core.publisher.Flux
|
import reactor.core.publisher.Flux
|
||||||
import reactor.core.publisher.Sinks
|
import reactor.core.publisher.Sinks
|
||||||
|
import java.net.URL
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.concurrent.Callable
|
import java.util.concurrent.Callable
|
||||||
@@ -330,39 +333,56 @@ class LibraryScanService(
|
|||||||
private fun downloadImages(games: List<Game>, progress: LibraryScanProgress): DownloadImagesResult {
|
private fun downloadImages(games: List<Game>, progress: LibraryScanProgress): DownloadImagesResult {
|
||||||
val completedImageDownload = AtomicInteger(0)
|
val completedImageDownload = AtomicInteger(0)
|
||||||
|
|
||||||
val imageDownloadTasks = games.map { game ->
|
// Collect all images from all games in the batch
|
||||||
Callable<Game?> {
|
val allImages = games.flatMap { game ->
|
||||||
|
val images = mutableListOf<Image>()
|
||||||
|
game.coverImage?.let { images.add(it) }
|
||||||
|
game.headerImage?.let { images.add(it) }
|
||||||
|
images.addAll(game.images)
|
||||||
|
images
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deduplicate by originalUrl
|
||||||
|
val uniqueImages = allImages
|
||||||
|
.filter { it.originalUrl != null }
|
||||||
|
.distinctBy { it.originalUrl }
|
||||||
|
|
||||||
|
// Map to track which Image entity was used for download per originalUrl
|
||||||
|
val downloadedImageMap = ConcurrentHashMap<URL, Image>()
|
||||||
|
|
||||||
|
// Download each unique image in parallel
|
||||||
|
val imageDownloadTasks = uniqueImages.map { image ->
|
||||||
|
Callable {
|
||||||
try {
|
try {
|
||||||
game.coverImage?.let {
|
imageService.downloadIfNew(image)
|
||||||
imageService.downloadIfNew(it)
|
image.originalUrl?.let { url ->
|
||||||
completedImageDownload.andIncrement
|
downloadedImageMap[url] = image
|
||||||
}
|
}
|
||||||
|
|
||||||
game.headerImage?.let {
|
|
||||||
imageService.downloadIfNew(it)
|
|
||||||
completedImageDownload.andIncrement
|
|
||||||
}
|
|
||||||
|
|
||||||
game.images.map {
|
|
||||||
imageService.downloadIfNew(it)
|
|
||||||
completedImageDownload.andIncrement
|
|
||||||
}
|
|
||||||
|
|
||||||
game
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log.error { "Error downloading images for game '${game.title}' (${game.id}): ${e.message}" }
|
log.error { "Error downloading image '${image.originalUrl}': ${e.message}" }
|
||||||
log.debug(e) {}
|
log.debug(e) {}
|
||||||
null
|
|
||||||
} finally {
|
} finally {
|
||||||
progress.currentStep.current = completedImageDownload.get()
|
progress.currentStep.current = completedImageDownload.incrementAndGet()
|
||||||
emit(progress)
|
emit(progress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
executor.invokeAll(imageDownloadTasks)
|
||||||
|
|
||||||
val gamesWithImages = executor.invokeAll(imageDownloadTasks).mapNotNull { it.get() }
|
// After downloads, associate the contentId with all other Image entities in the batch with the same originalUrl
|
||||||
|
for ((url, downloadedImage) in downloadedImageMap) {
|
||||||
|
val contentId = downloadedImage.contentId
|
||||||
|
if (contentId != null) {
|
||||||
|
allImages.filter { it.originalUrl.toString() == url.toString() && it !== downloadedImage }
|
||||||
|
.forEach { image ->
|
||||||
|
imageService.downloadIfNew(image)
|
||||||
|
progress.currentStep.current = completedImageDownload.incrementAndGet()
|
||||||
|
emit(progress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return DownloadImagesResult(gamesWithImages = gamesWithImages)
|
return DownloadImagesResult(gamesWithImages = games)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun calculateFileSizes(games: List<Game>, progress: LibraryScanProgress): CalculateFilesizesResult {
|
private fun calculateFileSizes(games: List<Game>, progress: LibraryScanProgress): CalculateFilesizesResult {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package org.gameyfin.app.users
|
package org.gameyfin.app.users
|
||||||
|
|
||||||
|
import org.gameyfin.app.config.ConfigProperties
|
||||||
|
import org.gameyfin.app.config.ConfigService
|
||||||
import org.gameyfin.app.core.Role
|
import org.gameyfin.app.core.Role
|
||||||
import org.gameyfin.app.users.entities.User
|
import org.gameyfin.app.users.entities.User
|
||||||
import org.gameyfin.app.users.persistence.UserRepository
|
import org.gameyfin.app.users.persistence.UserRepository
|
||||||
@@ -11,7 +13,8 @@ import org.springframework.stereotype.Service
|
|||||||
|
|
||||||
@Service
|
@Service
|
||||||
class RoleService(
|
class RoleService(
|
||||||
private val userRepository: UserRepository
|
private val userRepository: UserRepository,
|
||||||
|
private val configService: ConfigService
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -66,7 +69,8 @@ class RoleService(
|
|||||||
.filterIsInstance<OidcUserAuthority>()
|
.filterIsInstance<OidcUserAuthority>()
|
||||||
.flatMap { oidcUserAuthority ->
|
.flatMap { oidcUserAuthority ->
|
||||||
val userInfo = oidcUserAuthority.userInfo
|
val userInfo = oidcUserAuthority.userInfo
|
||||||
val roles = userInfo.getClaim<List<String>>("roles") ?: return@flatMap emptySequence()
|
val rolesClaim = configService.get(ConfigProperties.SSO.OIDC.RolesClaim)
|
||||||
|
val roles = userInfo.getClaim<List<String>>(rolesClaim) ?: return@flatMap emptySequence()
|
||||||
roles.asSequence().mapNotNull {
|
roles.asSequence().mapNotNull {
|
||||||
if (it.startsWith(SSO_ROLE_PREFIX)) SimpleGrantedAuthority(
|
if (it.startsWith(SSO_ROLE_PREFIX)) SimpleGrantedAuthority(
|
||||||
it.replace(SSO_ROLE_PREFIX, INTERNAL_ROLE_PREFIX)
|
it.replace(SSO_ROLE_PREFIX, INTERNAL_ROLE_PREFIX)
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
|
|||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
|
|
||||||
group = "org.gameyfin"
|
group = "org.gameyfin"
|
||||||
version = "2.0.0"
|
version = "2.0.1"
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# syntax=docker/dockerfile:1.4
|
||||||
|
FROM eclipse-temurin:21-jre
|
||||||
|
|
||||||
|
MAINTAINER grimsi
|
||||||
|
|
||||||
|
# Install necessary packages
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y tini gosu && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
ENV USER=gameyfin
|
||||||
|
|
||||||
|
RUN groupadd gameyfin && \
|
||||||
|
useradd -M -g gameyfin gameyfin
|
||||||
|
|
||||||
|
WORKDIR /opt/gameyfin
|
||||||
|
|
||||||
|
# Create necessary directories with appropriate permissions
|
||||||
|
RUN mkdir -p plugins db data logs && \
|
||||||
|
chown -R gameyfin:gameyfin .
|
||||||
|
|
||||||
|
# Copy entrypoint script and set permissions
|
||||||
|
COPY --chown=gameyfin:gameyfin ./docker/entrypoint.ubuntu.sh /entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
# Copy application jar (not ending with -plain.jar)
|
||||||
|
COPY --chown=gameyfin:gameyfin ./app/build/libs/ /tmp/app-libs/
|
||||||
|
RUN find /tmp/app-libs -type f -name "*.jar" ! -name "*-plain.jar" -exec cp {} gameyfin.jar \; && \
|
||||||
|
rm -rf /tmp/app-libs
|
||||||
|
|
||||||
|
# Copy all plugin jars
|
||||||
|
COPY --chown=gameyfin:gameyfin ./plugins/ /tmp/plugins/
|
||||||
|
RUN find /tmp/plugins -type f -path "*/build/libs/*.jar" -exec cp {} plugins/ \; && \
|
||||||
|
rm -rf /tmp/plugins
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/bin/tini", "--", "/entrypoint.sh"]
|
||||||
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ -n "$PUID" ] && [ -n "$PGID" ]; then
|
||||||
|
groupmod -o -g "$PGID" gameyfin
|
||||||
|
usermod -o -u "$PUID" gameyfin
|
||||||
|
chown -R gameyfin:gameyfin /opt/gameyfin
|
||||||
|
exec gosu gameyfin:gameyfin java -jar gameyfin.jar
|
||||||
|
else
|
||||||
|
exec gosu gameyfin:gameyfin java -jar gameyfin.jar
|
||||||
|
fi
|
||||||
|
|
||||||
+1
-1
@@ -150,7 +150,7 @@ class TorrentDownloadPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin
|
|||||||
|
|
||||||
private fun getTrackerUri(): URI {
|
private fun getTrackerUri(): URI {
|
||||||
val protocol = "http" // No SSL support in ttorrent: https://github.com/mpetazzoni/ttorrent/issues/4
|
val protocol = "http" // No SSL support in ttorrent: https://github.com/mpetazzoni/ttorrent/issues/4
|
||||||
val host = getHostname().getCanonicalHostName()
|
val host = getHostname().getHostName()
|
||||||
val port = config<Int>("trackerPort")
|
val port = config<Int>("trackerPort")
|
||||||
val path = "announce"
|
val path = "announce"
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
Plugin-Version: 1.0.0
|
Plugin-Version: 1.0.1
|
||||||
Plugin-Class: org.gameyfin.plugins.download.torrent.TorrentDownloadPlugin
|
Plugin-Class: org.gameyfin.plugins.download.torrent.TorrentDownloadPlugin
|
||||||
Plugin-Id: org.gameyfin.plugins.download.torrent
|
Plugin-Id: org.gameyfin.plugins.download.torrent
|
||||||
Plugin-Name: Torrent Download
|
Plugin-Name: Torrent Download
|
||||||
|
|||||||
Reference in New Issue
Block a user