mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-16 08:15:48 +00:00
Various bugfixes and minor improvements
This commit is contained in:
@@ -23,22 +23,13 @@ export default function LibraryCreationModal({
|
|||||||
const [scanAfterCreation, setScanAfterCreation] = useState<boolean>(true);
|
const [scanAfterCreation, setScanAfterCreation] = useState<boolean>(true);
|
||||||
|
|
||||||
async function createLibrary(library: LibraryDto) {
|
async function createLibrary(library: LibraryDto) {
|
||||||
try {
|
await LibraryEndpoint.createLibrary(library as LibraryDto, scanAfterCreation);
|
||||||
await LibraryEndpoint.createLibrary(library as LibraryDto, scanAfterCreation);
|
|
||||||
|
|
||||||
addToast({
|
addToast({
|
||||||
title: "New library created",
|
title: "New library created",
|
||||||
description: `Library ${library.name} created!`,
|
description: `Library ${library.name} created!`,
|
||||||
color: "success"
|
color: "success"
|
||||||
});
|
});
|
||||||
} catch (e) {
|
|
||||||
addToast({
|
|
||||||
title: "Error creating library",
|
|
||||||
description: `Library ${library.name} could not be created!`,
|
|
||||||
color: "warning"
|
|
||||||
});
|
|
||||||
throw "Error creating library: " + e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
+4
-2
@@ -38,9 +38,11 @@ class GameyfinExtensionFinder(pluginManager: PluginManager) : LegacyExtensionFin
|
|||||||
result.add(extensionWrapper)
|
result.add(extensionWrapper)
|
||||||
log.debug { "Added extension '$className' with ordinal ${extensionWrapper.ordinal}" }
|
log.debug { "Added extension '$className' with ordinal ${extensionWrapper.ordinal}" }
|
||||||
} catch (e: ClassNotFoundException) {
|
} catch (e: ClassNotFoundException) {
|
||||||
log.error(e) { e.message }
|
log.error { "Error loading plugin: ${e.message}" }
|
||||||
|
log.debug(e) {}
|
||||||
} catch (e: NoClassDefFoundError) {
|
} catch (e: NoClassDefFoundError) {
|
||||||
log.error(e) { e.message }
|
log.error { "Error loading plugin: ${e.message}" }
|
||||||
|
log.debug(e) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -215,7 +215,7 @@ class GameyfinPluginManager(
|
|||||||
|
|
||||||
fun getPluginForExtension(extensionClass: Class<ExtensionPoint>): PluginWrapper? {
|
fun getPluginForExtension(extensionClass: Class<ExtensionPoint>): PluginWrapper? {
|
||||||
return getPlugins().firstOrNull { pluginWrapper ->
|
return getPlugins().firstOrNull { pluginWrapper ->
|
||||||
getExtensionTypeClasses(pluginWrapper.pluginId).any { it == extensionClass.javaClass }
|
getExtensionClasses(pluginWrapper.pluginId).any { it == extensionClass }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import org.gameyfin.app.core.alphaNumeric
|
|||||||
import org.gameyfin.app.core.filesystem.FilesystemService
|
import org.gameyfin.app.core.filesystem.FilesystemService
|
||||||
import org.gameyfin.app.core.filterValuesNotNull
|
import org.gameyfin.app.core.filterValuesNotNull
|
||||||
import org.gameyfin.app.core.plugins.PluginService
|
import org.gameyfin.app.core.plugins.PluginService
|
||||||
|
import org.gameyfin.app.core.plugins.management.GameyfinPluginDescriptor
|
||||||
import org.gameyfin.app.core.plugins.management.GameyfinPluginManager
|
import org.gameyfin.app.core.plugins.management.GameyfinPluginManager
|
||||||
import org.gameyfin.app.core.plugins.management.PluginManagementEntry
|
import org.gameyfin.app.core.plugins.management.PluginManagementEntry
|
||||||
import org.gameyfin.app.core.replaceRomanNumerals
|
import org.gameyfin.app.core.replaceRomanNumerals
|
||||||
@@ -104,7 +105,8 @@ class GameService(
|
|||||||
imageService.downloadIfNew(it)
|
imageService.downloadIfNew(it)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log.error(e) { "Error downloading images for game: ${e.message}" }
|
log.error { "Error downloading images for game: ${e.message}" }
|
||||||
|
log.debug(e) {}
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,7 +451,9 @@ class GameService(
|
|||||||
try {
|
try {
|
||||||
plugin.fetchByTitle(searchTerm, 10).map { plugin to it }
|
plugin.fetchByTitle(searchTerm, 10).map { plugin to it }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log.error(e) { "Error fetching metadata for searchterm '$searchTerm' with plugin ${plugin.javaClass.name}" }
|
val pluginWrapper = pluginManager.getPluginForExtension(plugin.javaClass)
|
||||||
|
log.warn { "Error fetching metadata for searchterm '$searchTerm' with plugin '${(pluginWrapper?.descriptor as GameyfinPluginDescriptor?)?.pluginName ?: pluginWrapper?.pluginId ?: plugin.javaClass.name}': ${e.message}" }
|
||||||
|
log.debug(e) {}
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -561,7 +565,9 @@ class GameService(
|
|||||||
try {
|
try {
|
||||||
return@async plugin.fetchById(originalId)
|
return@async plugin.fetchById(originalId)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log.error(e) { "Error fetching metadata for game [id: $originalId] with plugin ${plugin.javaClass.name}" }
|
val pluginWrapper = pluginManager.getPluginForExtension(plugin.javaClass)
|
||||||
|
log.warn { "Error fetching metadata for game [id: $originalId] with plugin '${(pluginWrapper?.descriptor as GameyfinPluginDescriptor?)?.pluginName ?: pluginWrapper?.pluginId ?: plugin.javaClass.name}': ${e.message}" }
|
||||||
|
log.debug(e) {}
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}.await()
|
}.await()
|
||||||
@@ -638,8 +644,10 @@ class GameService(
|
|||||||
executor.submit<PluginApiMetadata?> {
|
executor.submit<PluginApiMetadata?> {
|
||||||
try {
|
try {
|
||||||
plugin.fetchByTitle(gameTitle).firstOrNull()
|
plugin.fetchByTitle(gameTitle).firstOrNull()
|
||||||
} catch (_: Exception) {
|
} catch (e: Exception) {
|
||||||
log.error { "Error fetching metadata for game title '$gameTitle' with plugin ${plugin.javaClass.name}" }
|
val pluginWrapper = pluginManager.getPluginForExtension(plugin.javaClass)
|
||||||
|
log.warn { "Error fetching metadata for game title '$gameTitle' with plugin '${(pluginWrapper?.descriptor as GameyfinPluginDescriptor?)?.pluginName ?: pluginWrapper?.pluginId ?: plugin.javaClass.name}': ${e.message}" }
|
||||||
|
log.debug(e) {}
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,13 @@ class Company(
|
|||||||
if (other !is Company) return false
|
if (other !is Company) return false
|
||||||
return name == other.name && type == other.type
|
return name == other.name && type == other.type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = id?.hashCode() ?: 0
|
||||||
|
result = 31 * result + name.hashCode()
|
||||||
|
result = 31 * result + type.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class CompanyType {
|
enum class CompanyType {
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ class Image(
|
|||||||
if (other !is Image) return false
|
if (other !is Image) return false
|
||||||
return originalUrl.toString() == other.originalUrl.toString()
|
return originalUrl.toString() == other.originalUrl.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = id?.hashCode() ?: 0
|
||||||
|
result = 31 * result + originalUrl?.toString().hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class ImageType {
|
enum class ImageType {
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
package org.gameyfin.app.games.repositories
|
package org.gameyfin.app.games.repositories
|
||||||
|
|
||||||
import org.gameyfin.app.games.entities.Game
|
import org.gameyfin.app.games.entities.Game
|
||||||
import org.springframework.data.domain.Limit
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
|
||||||
interface GameRepository : JpaRepository<Game, Long> {
|
interface GameRepository : JpaRepository<Game, Long>
|
||||||
fun findByMetadata_Path(path: String): Game?
|
|
||||||
fun findAllByMetadata_PathIn(paths: List<String>): List<Game>
|
|
||||||
fun findByOrderByCreatedAtDesc(limit: Limit): List<Game>
|
|
||||||
fun findByOrderByUpdatedAtDesc(limit: Limit): List<Game>
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
package org.gameyfin.app.libraries
|
package org.gameyfin.app.libraries
|
||||||
|
|
||||||
import jakarta.persistence.Entity
|
import jakarta.persistence.*
|
||||||
import jakarta.persistence.GeneratedValue
|
|
||||||
import jakarta.persistence.GenerationType
|
|
||||||
import jakarta.persistence.Id
|
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
class DirectoryMapping(
|
class DirectoryMapping(
|
||||||
@@ -12,6 +9,7 @@ class DirectoryMapping(
|
|||||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||||
var id: Long? = null,
|
var id: Long? = null,
|
||||||
|
|
||||||
|
@Column(unique = true)
|
||||||
var internalPath: String,
|
var internalPath: String,
|
||||||
|
|
||||||
var externalPath: String? = null,
|
var externalPath: String? = null,
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
package org.gameyfin.app.libraries
|
package org.gameyfin.app.libraries
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.EntityGraph
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
import org.springframework.data.jpa.repository.Query
|
import org.springframework.data.jpa.repository.Query
|
||||||
|
|
||||||
interface LibraryRepository : JpaRepository<Library, Long> {
|
interface LibraryRepository : JpaRepository<Library, Long> {
|
||||||
@EntityGraph(attributePaths = ["games"])
|
@Query("SELECT d.internalPath, l FROM Library l JOIN l.directories d WHERE d.internalPath IN :paths")
|
||||||
@Query("SELECT l FROM Library l ORDER BY function('RAND') LIMIT 1")
|
fun findByPaths(paths: List<String>): List<Pair<String, Library>>
|
||||||
fun findRandomLibrary(): Library?
|
|
||||||
}
|
}
|
||||||
@@ -183,7 +183,8 @@ class LibraryScanService(
|
|||||||
)
|
)
|
||||||
emit(progress)
|
emit(progress)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log.error(e) { "Error during quick scan for library ${library.id}: ${e.message}" }
|
log.error { "Error during quick scan for library ${library.id}: ${e.message}" }
|
||||||
|
log.debug(e) {}
|
||||||
progress.status = LibraryScanStatus.FAILED
|
progress.status = LibraryScanStatus.FAILED
|
||||||
progress.finishedAt = Instant.now()
|
progress.finishedAt = Instant.now()
|
||||||
emit(progress)
|
emit(progress)
|
||||||
@@ -282,7 +283,8 @@ class LibraryScanService(
|
|||||||
)
|
)
|
||||||
emit(progress)
|
emit(progress)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log.error(e) { "Error during full scan for library ${library.id}: ${e.message}" }
|
log.error { "Error during full scan for library ${library.id}: ${e.message}" }
|
||||||
|
log.debug(e) {}
|
||||||
progress.status = LibraryScanStatus.FAILED
|
progress.status = LibraryScanStatus.FAILED
|
||||||
progress.finishedAt = Instant.now()
|
progress.finishedAt = Instant.now()
|
||||||
emit(progress)
|
emit(progress)
|
||||||
@@ -312,7 +314,8 @@ class LibraryScanService(
|
|||||||
|
|
||||||
return@Callable game
|
return@Callable game
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log.error(e) { "Error processing game: ${e.message}" }
|
log.error { "Error processing game: ${e.message}" }
|
||||||
|
log.debug(e) {}
|
||||||
newUnmatchedPaths.add(path.toString())
|
newUnmatchedPaths.add(path.toString())
|
||||||
|
|
||||||
return@Callable null
|
return@Callable null
|
||||||
@@ -372,7 +375,8 @@ class LibraryScanService(
|
|||||||
|
|
||||||
game
|
game
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log.error(e) { "Error downloading images for game: ${e.message}" }
|
log.error { "Error downloading images for game: ${e.message}" }
|
||||||
|
log.debug(e) {}
|
||||||
null
|
null
|
||||||
} finally {
|
} finally {
|
||||||
progress.currentStep.current = completedImageDownload.get()
|
progress.currentStep.current = completedImageDownload.get()
|
||||||
@@ -437,7 +441,8 @@ class LibraryScanService(
|
|||||||
val game = gameService.update(game)
|
val game = gameService.update(game)
|
||||||
return@Callable game
|
return@Callable game
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log.error(e) { "Error updating game with id '${game.id}': ${e.message}" }
|
log.error { "Error updating game with id '${game.id}': ${e.message}" }
|
||||||
|
log.debug(e) {}
|
||||||
return@Callable null
|
return@Callable null
|
||||||
} finally {
|
} finally {
|
||||||
progress.currentStep.current = completedUpdates.incrementAndGet()
|
progress.currentStep.current = completedUpdates.incrementAndGet()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.gameyfin.app.libraries
|
package org.gameyfin.app.libraries
|
||||||
|
|
||||||
|
import com.vaadin.hilla.exception.EndpointException
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import org.gameyfin.app.games.GameService
|
import org.gameyfin.app.games.GameService
|
||||||
import org.gameyfin.app.libraries.dto.LibraryDto
|
import org.gameyfin.app.libraries.dto.LibraryDto
|
||||||
@@ -19,7 +20,7 @@ class LibraryService(
|
|||||||
private val libraryRepository: LibraryRepository,
|
private val libraryRepository: LibraryRepository,
|
||||||
private val libraryCoreService: LibraryCoreService,
|
private val libraryCoreService: LibraryCoreService,
|
||||||
private val libraryScanService: LibraryScanService,
|
private val libraryScanService: LibraryScanService,
|
||||||
private val gameService: GameService,
|
private val gameService: GameService
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -70,6 +71,9 @@ class LibraryService(
|
|||||||
* @return The created or updated LibraryDto object.
|
* @return The created or updated LibraryDto object.
|
||||||
*/
|
*/
|
||||||
fun create(library: LibraryDto, scanAfterCreation: Boolean) {
|
fun create(library: LibraryDto, scanAfterCreation: Boolean) {
|
||||||
|
// Check for duplicate directories before creating a new library
|
||||||
|
checkForDuplicateDirectories(library.directories.map { it.internalPath })
|
||||||
|
|
||||||
val newLibrary = libraryRepository.save(libraryCoreService.toEntity(library))
|
val newLibrary = libraryRepository.save(libraryCoreService.toEntity(library))
|
||||||
|
|
||||||
if (scanAfterCreation) {
|
if (scanAfterCreation) {
|
||||||
@@ -88,20 +92,49 @@ class LibraryService(
|
|||||||
val library = libraryRepository.findByIdOrNull(libraryUpdateDto.id)
|
val library = libraryRepository.findByIdOrNull(libraryUpdateDto.id)
|
||||||
?: throw IllegalArgumentException("Library with ID $libraryUpdateDto.id not found")
|
?: throw IllegalArgumentException("Library with ID $libraryUpdateDto.id not found")
|
||||||
|
|
||||||
// Update only non-null fields
|
|
||||||
libraryUpdateDto.name?.let { library.name = it }
|
libraryUpdateDto.name?.let { library.name = it }
|
||||||
libraryUpdateDto.directories?.let {
|
libraryUpdateDto.directories?.let { updatedDirs ->
|
||||||
library.directories.clear()
|
checkForDuplicateDirectories(
|
||||||
library.directories.addAll(
|
updatedDirs.map { it.internalPath },
|
||||||
it.map { d -> DirectoryMapping(internalPath = d.internalPath, externalPath = d.externalPath) }
|
excludeLibraryId = library.id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val existingMappings = library.directories.associateBy { it.internalPath }
|
||||||
|
val updatedInternalPaths = updatedDirs.map { it.internalPath }.toSet()
|
||||||
|
|
||||||
|
// Remove mappings not present in the update
|
||||||
|
val removedDirs = library.directories.filter { it.internalPath !in updatedInternalPaths }
|
||||||
|
library.directories.removeAll(removedDirs)
|
||||||
|
|
||||||
|
// Remove all games within removed directories
|
||||||
|
val removedDirPaths = removedDirs.map { it.internalPath }
|
||||||
|
library.games.removeIf { game ->
|
||||||
|
removedDirPaths.any { removedPath ->
|
||||||
|
game.metadata.path.startsWith(removedPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update existing or add new directory mappings
|
||||||
|
updatedDirs.forEach { dto ->
|
||||||
|
val mapping = existingMappings[dto.internalPath]
|
||||||
|
if (mapping != null) {
|
||||||
|
mapping.externalPath = dto.externalPath // update fields
|
||||||
|
} else {
|
||||||
|
library.directories.add(
|
||||||
|
DirectoryMapping(
|
||||||
|
internalPath = dto.internalPath,
|
||||||
|
externalPath = dto.externalPath
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
libraryUpdateDto.unmatchedPaths?.let {
|
libraryUpdateDto.unmatchedPaths?.let {
|
||||||
library.unmatchedPaths.clear()
|
library.unmatchedPaths.clear()
|
||||||
library.unmatchedPaths.addAll(it)
|
library.unmatchedPaths.addAll(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
library.updatedAt = Instant.now() // Force the EntityListener to trigger an update and update the timestamp
|
library.updatedAt = Instant.now()
|
||||||
libraryRepository.save(library)
|
libraryRepository.save(library)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,4 +146,17 @@ class LibraryService(
|
|||||||
fun delete(libraryId: Long) {
|
fun delete(libraryId: Long) {
|
||||||
libraryRepository.deleteById(libraryId)
|
libraryRepository.deleteById(libraryId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun checkForDuplicateDirectories(newLibraryFolders: List<String>, excludeLibraryId: Long? = null) {
|
||||||
|
val alreadyConfiguredFolders = libraryRepository.findByPaths(newLibraryFolders)
|
||||||
|
.filter { it.second.id != excludeLibraryId } // Exclude the current library if updating
|
||||||
|
.map { Pair(it.first, it.second) } // Convert to Pair for easier error message formatting
|
||||||
|
|
||||||
|
if (alreadyConfiguredFolders.isNotEmpty()) {
|
||||||
|
throw EndpointException(
|
||||||
|
"The following directories are already mapped to another library: " +
|
||||||
|
alreadyConfiguredFolders.joinToString(", ") { "${it.first} (${it.second.name})" }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,12 @@
|
|||||||
package org.gameyfin.app.messages
|
package org.gameyfin.app.messages
|
||||||
|
|
||||||
import org.gameyfin.app.messages.templates.MessageTemplates
|
|
||||||
import org.gameyfin.app.users.UserService
|
|
||||||
import io.github.oshai.kotlinlogging.KLogger
|
import io.github.oshai.kotlinlogging.KLogger
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import org.gameyfin.app.core.events.AccountDeletedEvent
|
import org.gameyfin.app.core.events.*
|
||||||
import org.gameyfin.app.core.events.AccountStatusChangedEvent
|
|
||||||
import org.gameyfin.app.core.events.EmailNeedsConfirmationEvent
|
|
||||||
import org.gameyfin.app.core.events.PasswordResetRequestEvent
|
|
||||||
import org.gameyfin.app.core.events.RegistrationAttemptWithExistingEmailEvent
|
|
||||||
import org.gameyfin.app.core.events.UserInvitationEvent
|
|
||||||
import org.gameyfin.app.core.events.UserRegistrationWaitingForApprovalEvent
|
|
||||||
import org.gameyfin.app.messages.providers.AbstractMessageProvider
|
import org.gameyfin.app.messages.providers.AbstractMessageProvider
|
||||||
import org.gameyfin.app.messages.templates.MessageTemplateService
|
import org.gameyfin.app.messages.templates.MessageTemplateService
|
||||||
|
import org.gameyfin.app.messages.templates.MessageTemplates
|
||||||
|
import org.gameyfin.app.users.UserService
|
||||||
import org.springframework.context.ApplicationContext
|
import org.springframework.context.ApplicationContext
|
||||||
import org.springframework.context.event.EventListener
|
import org.springframework.context.event.EventListener
|
||||||
import org.springframework.scheduling.annotation.Async
|
import org.springframework.scheduling.annotation.Async
|
||||||
@@ -72,7 +66,8 @@ class MessageService(
|
|||||||
val template = templateService.getMessageTemplate(templateKey)
|
val template = templateService.getMessageTemplate(templateKey)
|
||||||
sendNotification(user.email, "[Gameyfin] Test Notification", template, placeholders)
|
sendNotification(user.email, "[Gameyfin] Test Notification", template, placeholders)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log.error(e) { "Failed to send test message" }
|
log.error { "Failed to send test message: ${e.message}" }
|
||||||
|
log.debug(e) {}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user