Skip to content

Commit

Permalink
Merge pull request #54 from SettingDust/feature/typed-add
Browse files Browse the repository at this point in the history
feat: allow user specify project type when adding project
  • Loading branch information
juraj-hrivnak authored Oct 29, 2024
2 parents 6ff15b4 + 2090a1b commit b1bd3b8
Show file tree
Hide file tree
Showing 15 changed files with 148 additions and 62 deletions.
3 changes: 3 additions & 0 deletions docs/topics/pakku-add-prj.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ Specify the project precisely
`--gh`, `--github`
: GitHub repository URL or `{owner}/{repo}`

`-t`, `--type=(mod|resource_pack|data_pack|world|shader)`
: Project type of project to add

</snippet>

`-h`, `--help`
Expand Down
3 changes: 3 additions & 0 deletions docs/topics/pakku-add.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ Add projects
`-D`, `--no-deps`
: Ignore resolving dependencies

`-t`, `--type=(mod|resource_pack|data_pack|world|shader)`
: Project type of projects to add

</snippet>

`-h`, `--help`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import teksturepako.pakku.api.data.LockFile
import teksturepako.pakku.api.platforms.GitHub
import teksturepako.pakku.api.platforms.Platform
import teksturepako.pakku.api.projects.Project
import teksturepako.pakku.api.projects.ProjectType

data class RequestHandlers(
val onError: suspend (error: ActionError) -> Unit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ suspend fun updateMultipleProjectsWithFiles(
.filter { GitHub.serialName in it.slug.keys }
.map { oldProject ->
val ghSlug = oldProject.slug[GitHub.serialName] ?: return@map oldProject
GitHub.requestProjectWithFiles(emptyList(), emptyList(), ghSlug)
GitHub.requestProjectWithFiles(emptyList(), emptyList(), ghSlug, projectType = oldProject.type)
?.inheritPropertiesFrom(configFile)
?.takeIf { it.hasFiles() }
?: oldProject
Expand All @@ -40,7 +40,7 @@ suspend fun updateMultipleProjectsWithFiles(
val platformProjects = platform.requestMultipleProjectsWithFiles(
mcVersions,
loaders,
accProjects.mapNotNull { it.id[platform.serialName] },
accProjects.mapNotNull { project -> project.id[platform.serialName]?.let { it to project.type } }.toMap(),
Int.MAX_VALUE
).inheritPropertiesFrom(configFile)

Expand Down Expand Up @@ -81,6 +81,7 @@ private fun combineProjects(accProject: Project, newProject: Project, platformNa
val updatedFiles = (newFiles.take(numberOfFiles) + accProject.files).filterNot { projectFile ->
projectFile.type == platformName && projectFile.datePublished < accPublished
}
.distinctBy { it.type }
.toMutableSet()

return (accProject + newProject).get()?.copy(files = updatedFiles)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ data class CfModpackModel(
val projects = CurseForge.requestMultipleProjects(this.files.map { it.projectID.toString() })
val projectFiles = CurseForge.requestMultipleProjectFiles(lockFile.getMcVersions(),
lockFile.getLoaders(),
projects.mapNotNull { project -> project.slug[CurseForge.serialName]?.let { it to project.type } }.toMap(),
this.files.map { it.fileID.toString() })

projects.assignFiles(projectFiles, CurseForge)
Expand All @@ -55,8 +56,8 @@ data class CfModpackModel(
debug { println("Modrinth sub-import") }

val slugs = projects.mapNotNull { project ->
project.slug[CurseForge.serialName]
}
project.slug[CurseForge.serialName]?.let { it to project.type }
}.toMap()

val mrProjects = Modrinth.requestMultipleProjectsWithFiles(
lockFile.getMcVersions(), lockFile.getLoaders(), slugs, 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,15 @@ data class MrModpackModel(
runBlocking {
debug { println("CurseForge sub-import") }

val slugs = projects.mapNotNull { project ->
project.slug[Modrinth.serialName]
val projectToSlugs = projects.mapNotNull { project ->
project.slug[Modrinth.serialName]?.let { project to it }
}

val cfProjects = slugs.map { slug ->
val cfProjects = projectToSlugs.map { (project, slug) ->
async {
CurseForge.requestProjectFromSlug(slug)?.apply {
files += CurseForge.requestFilesForProject(
lockFile.getMcVersions(), lockFile.getLoaders(), this
lockFile.getMcVersions(), lockFile.getLoaders(), this, projectType = project.type
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,11 @@ object CurseForge : Platform(

// -- PROJECT --

override suspend fun requestProject(input: String): Project? = when
override suspend fun requestProject(input: String, projectType: ProjectType?): Project? = when
{
input.matches("[0-9]{5,6}".toRegex()) -> requestProjectFromId(input)
else -> requestProjectFromSlug(input)
}
}.also { project -> projectType?.let { project?.type = it } }

private fun CfModModel.toProject(): Project?
{
Expand Down Expand Up @@ -129,7 +129,7 @@ object CurseForge : Platform(
} ?: true // If no loaders found, accept model
}

internal fun compareByLoaders(loaders: List<String>) = { file: CfModModel.File ->
internal fun compareByLoaders(loaders: List<String>): (CfModModel.File) -> Comparable<*> = { file: CfModModel.File ->
val fileLoaders = file.sortableGameVersions.filter { it.gameVersionTypeId == LOADER_VERSION_TYPE_ID }
.map { it.gameVersionName.lowercase() }
loaders.indexOfFirst { it in fileLoaders }.let { if (it == -1) loaders.size else it }
Expand Down Expand Up @@ -174,7 +174,7 @@ object CurseForge : Platform(
}

override suspend fun requestProjectFiles(
mcVersions: List<String>, loaders: List<String>, projectId: String, fileId: String?
mcVersions: List<String>, loaders: List<String>, projectId: String, fileId: String?, projectType: ProjectType?
): MutableSet<ProjectFile>
{
// Handle optional fileId
Expand Down Expand Up @@ -219,7 +219,7 @@ object CurseForge : Platform(
}

override suspend fun requestMultipleProjectFiles(
mcVersions: List<String>, loaders: List<String>, ids: List<String>
mcVersions: List<String>, loaders: List<String>, projectIdsToTypes: Map<String, ProjectType?>, ids: List<String>
): MutableSet<ProjectFile>
{
// Handle mcVersions
Expand All @@ -240,19 +240,19 @@ object CurseForge : Platform(
}

override suspend fun requestMultipleProjectsWithFiles(
mcVersions: List<String>, loaders: List<String>, ids: List<String>, numberOfFiles: Int
mcVersions: List<String>, loaders: List<String>, projectIdsToTypes: Map<String, ProjectType?>, numberOfFiles: Int
): MutableSet<Project>
{
val response = json.decodeFromString<GetMultipleProjectsResponse>(
this.requestProjectBody("mods", MultipleProjectsRequest(ids.map(String::toInt)))
this.requestProjectBody("mods", MultipleProjectsRequest(projectIdsToTypes.keys.map(String::toInt)))
?: return mutableSetOf()
).data

val fileIds = response.flatMap { model ->
model.latestFilesIndexes.map { it.fileId.toString() }
}

val projectFiles = requestMultipleProjectFiles(mcVersions, loaders, fileIds)
val projectFiles = requestMultipleProjectFiles(mcVersions, loaders, projectIdsToTypes, fileIds)
val projects = response.mapNotNull { it.toProject() }

projects.assignFiles(projectFiles, this)
Expand Down
16 changes: 10 additions & 6 deletions src/commonMain/kotlin/teksturepako/pakku/api/platforms/GitHub.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,11 @@ object GitHub : Http(), Provider
)
}

override suspend fun requestProject(input: String): Project?
override suspend fun requestProject(input: String, projectType: ProjectType?): Project?
{
return json.decodeFromString<GhRepoModel>(
this.requestBody("https://api.github.com/repos/$input")
?: return null
).toProject()
this.requestBody("https://api.github.com/repos/$input") ?: return null
).toProject().also { project -> projectType?.let { project.type = it } }
}

private fun GhReleaseModel.toProjectFiles(parentId: String): List<ProjectFile>
Expand Down Expand Up @@ -63,10 +62,15 @@ object GitHub : Http(), Provider
}

override suspend fun requestProjectWithFiles(
mcVersions: List<String>, loaders: List<String>, input: String, fileId: String?, numberOfFiles: Int
mcVersions: List<String>,
loaders: List<String>,
input: String,
fileId: String?,
numberOfFiles: Int,
projectType: ProjectType?
): Project?
{
val project = requestProject(input) ?: return null
val project = requestProject(input, projectType) ?: return null

val projectFiles = if (fileId == null)
{
Expand Down
48 changes: 36 additions & 12 deletions src/commonMain/kotlin/teksturepako/pakku/api/platforms/Modrinth.kt
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,12 @@ object Modrinth : Platform(

// -- PROJECT --

override suspend fun requestProject(input: String): Project? = when
override suspend fun requestProject(input: String, projectType: ProjectType?): Project? = when
{
input.matches("[0-9]{6}".toRegex()) -> null
input.matches("\b[0-9a-zA-Z]{8}\b".toRegex()) -> requestProjectFromId(input)
else -> requestProjectFromSlug(input)
}
}.also { project -> projectType?.let { project?.type = it } }

private fun MrProjectModel.toProject(): Project?
{
Expand Down Expand Up @@ -144,7 +144,11 @@ object Modrinth : Platform(
} ?: true // If no loaders found, accept model
}

internal fun compareByLoaders(loaders: List<String>) = { version: MrVersionModel ->
internal fun compareByLoaders(loaders: List<String>): (MrVersionModel) -> Comparable<*> = if (loaders.size <= 1)
{
{ 0 }
}
else { version: MrVersionModel ->
loaders.indexOfFirst { it in version.loaders }.let { if (it == -1) loaders.size else it }
}

Expand Down Expand Up @@ -177,18 +181,26 @@ object Modrinth : Platform(
}.asReversed() // Reverse to make non source files first
}

private const val DATAPACK_LOADER = "datapack"

override suspend fun requestProjectFiles(
mcVersions: List<String>, loaders: List<String>, projectId: String, fileId: String?
mcVersions: List<String>, loaders: List<String>, projectId: String, fileId: String?, projectType: ProjectType?
): MutableSet<ProjectFile>
{
val actualLoaders = when (projectType)
{
ProjectType.DATA_PACK -> listOf(DATAPACK_LOADER)
else -> loaders
}

return if (fileId == null)
{
// Multiple files
json.decodeFromString<List<MrVersionModel>>(
this.requestProjectBody("project/$projectId/version") ?: return mutableSetOf()
)
.filterFileModels(mcVersions, loaders)
.sortedWith(compareBy(compareByLoaders(loaders)))
.filterFileModels(mcVersions, actualLoaders)
.sortedWith(compareBy(compareByLoaders(actualLoaders)))
.flatMap { version -> version.toProjectFiles() }
.debugIfEmpty {
println("${this::class.simpleName}#requestProjectFiles: file is null")
Expand All @@ -206,8 +218,10 @@ object Modrinth : Platform(
}

override suspend fun requestMultipleProjectFiles(
mcVersions: List<String>, loaders: List<String>, ids: List<String>
mcVersions: List<String>, loaders: List<String>, projectIdsToTypes: Map<String, ProjectType?>, ids: List<String>
): MutableSet<ProjectFile> = coroutineScope {
val loadersWithType =
(if (projectIdsToTypes.values.any { it == ProjectType.DATA_PACK }) listOf(DATAPACK_LOADER) else listOf()) + loaders
// Chunk requests if there are too many ids; Also do this in parallel
return@coroutineScope ids.chunked(1_000).map { list ->
async {
Expand All @@ -220,24 +234,34 @@ object Modrinth : Platform(
}
.awaitAll()
.flatten()
.filterFileModels(mcVersions, loaders)
.sortedWith(compareBy ({ Instant.parse(it.datePublished) }, compareByLoaders(loaders)))
.filterFileModels(mcVersions, loadersWithType)
.sortedWith(
compareByDescending<MrVersionModel> { Instant.parse(it.datePublished) }
.thenBy { file ->
compareByLoaders(projectIdsToTypes[file.projectId]?.let {
when (it)
{
ProjectType.DATA_PACK -> listOf(DATAPACK_LOADER)
else -> null
}
} ?: loaders)(file)
})
.flatMap { version -> version.toProjectFiles() }
.toMutableSet()
}

override suspend fun requestMultipleProjectsWithFiles(
mcVersions: List<String>, loaders: List<String>, ids: List<String>, numberOfFiles: Int
mcVersions: List<String>, loaders: List<String>, projectIdsToTypes: Map<String, ProjectType?>, numberOfFiles: Int
): MutableSet<Project>
{
val url = encode("projects?ids=${ids.map { "\"$it\"" }}".filterNot { it.isWhitespace() }, allow = "?=")
val url = encode("projects?ids=${projectIdsToTypes.keys.map { "\"$it\"" }}".filterNot { it.isWhitespace() }, allow = "?=")

val response = json.decodeFromString<List<MrProjectModel>>(
this.requestProjectBody(url) ?: return mutableSetOf()
)

val fileIds = response.flatMap { it.versions }
val projectFiles = requestMultipleProjectFiles(mcVersions, loaders, fileIds)
val projectFiles = requestMultipleProjectFiles(mcVersions, loaders, projectIdsToTypes, fileIds)
val projects = response.mapNotNull { it.toProject() }

projects.assignFiles(projectFiles, this)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package teksturepako.pakku.api.platforms

import com.github.michaelbull.result.get
import teksturepako.pakku.api.projects.Project
import teksturepako.pakku.api.projects.ProjectType

object Multiplatform : Provider
{
Expand Down Expand Up @@ -29,12 +30,13 @@ object Multiplatform : Provider
* project is found on either platform.
*
* @param input The project ID or slug.
* @param projectType The type of project.
* @return A [project][Project] containing data retrieved from all platforms, or null if no data is found.
*/
override suspend fun requestProject(input: String): Project?
override suspend fun requestProject(input: String, projectType: ProjectType?): Project?
{
var cf = CurseForge.requestProject(input)
var mr = Modrinth.requestProject(input)
var cf = CurseForge.requestProject(input, projectType)
var mr = Modrinth.requestProject(input, projectType)

// Retrieve project from another platform if it's missing.
if (cf == null && mr != null)
Expand All @@ -48,7 +50,9 @@ object Multiplatform : Provider

// Combine projects or return just one of them.
return cf?.let { c ->
projectType?.let { c.type = it }
mr?.let { m ->
projectType?.let { m.type = it }
(c + m).get() // Combine projects if project is available from both platforms.
} ?: c // Return the CurseForge project if Modrinth project is missing.
} ?: mr // Return the Modrinth project if CurseForge project is missing.
Expand All @@ -65,23 +69,28 @@ object Multiplatform : Provider
* @return A [project][Project] with files from all platforms or null if the initial project request is unsuccessful.
*/
override suspend fun requestProjectWithFiles(
mcVersions: List<String>, loaders: List<String>, input: String, fileId: String?, numberOfFiles: Int
mcVersions: List<String>,
loaders: List<String>,
input: String,
fileId: String?,
numberOfFiles: Int ,
projectType: ProjectType?
): Project?
{
val project = requestProject(input) ?: return null
val project = requestProject(input, projectType) ?: return null

if (fileId == null)
{
for (platform in platforms)
{
project.files.addAll(platform.requestFilesForProject(mcVersions, loaders, project, null, numberOfFiles))
project.files.addAll(platform.requestFilesForProject(mcVersions, loaders, project, null, numberOfFiles, projectType))
}
}
else
{
if (project.isOnPlatform(CurseForge))
{
val cfFile = CurseForge.requestProjectFiles(mcVersions, loaders, project.id[CurseForge.serialName]!!, fileId).firstOrNull()
val cfFile = CurseForge.requestProjectFiles(mcVersions, loaders, project.id[CurseForge.serialName]!!, fileId, projectType).firstOrNull()

val hash = cfFile?.hashes?.get("sha1")

Expand All @@ -100,7 +109,7 @@ object Multiplatform : Provider

if (project.isOnPlatform(Modrinth))
{
val mrFile = Modrinth.requestProjectFiles(mcVersions, loaders, project.id[Modrinth.serialName]!!, fileId).firstOrNull()
val mrFile = Modrinth.requestProjectFiles(mcVersions, loaders, project.id[Modrinth.serialName]!!, fileId, projectType).firstOrNull()

val bytes = mrFile?.url?.let { Modrinth.requestByteArray(it) }

Expand Down
Loading

0 comments on commit b1bd3b8

Please sign in to comment.