BloodyMods / ServerStarter

MIT License
100 stars 64 forks source link

Are you kidding me... no you not (FIX FOR API REMOVAL) #72

Open anonymusdennis opened 1 year ago

anonymusdennis commented 1 year ago

@BloodWorkXGaming Kannst du das Vlt. fixen??? I went on for 3 Hours making an older version of this compatible with the new API and now i am here to see it has already been done??? aww man i need to sleep now i literally sat there for 4 hours implementing the new api because i thought i had to do it myself... anyways i did this:

--- Edit -> I downloaded the serverstarter from here to find out my pain was not in vain The following is a Problem: the page you used in your project as an api got removed I edited the code so that this Error:

[12:31:02] [ERROR] Error while trying to get URL from cursemeta for mod ModEntryRaw(projectID=405744, fileID=3296428)

java.io.IOException: Request to https://cursemeta.dries007.net/405744/3296428.json was not successful.
        at atm.bloodworkxgaming.serverstarter.packtype.curse.CursePackType.downloadMods$lambda-3(CursePackType.kt:203)
        at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183)
        at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1625)
        at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
        at java.base/java.util.stream.ForEachOps$ForEachTask.compute(ForEachOps.java:290)
        at java.base/java.util.concurrent.CountedCompleter.exec(CountedCompleter.java:754)
        at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373)
        at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182)
        at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655)
        at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622)
        at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165)

Does not happen anymore you may use this code in your project you are welcome

(maaan i really felt so dumb when i read the latest push: moved to new api, But it turns out i wasn't wrong...)

package atm.bloodworkxgaming.serverstarter.packtype.curse

import atm.bloodworkxgaming.serverstarter.InternetManager
import atm.bloodworkxgaming.serverstarter.ServerStarter.Companion.LOGGER
import atm.bloodworkxgaming.serverstarter.config.ConfigFile
import atm.bloodworkxgaming.serverstarter.packtype.AbstractZipbasedPackType
import atm.bloodworkxgaming.serverstarter.packtype.writeToFile
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import okhttp3.Request
import org.apache.commons.io.FileUtils
import org.apache.commons.io.FilenameUtils
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.io.InputStreamReader
import java.net.URISyntaxException
import java.nio.file.PathMatcher
import java.nio.file.Paths
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicInteger
import java.util.regex.Pattern
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream

open class CursePackType(private val configFile: ConfigFile) : AbstractZipbasedPackType(configFile) {
    private var forgeVersion: String = configFile.install.loaderVersion
    private var mcVersion: String = configFile.install.mcVersion
    private val oldFiles = File(basePath + "OLD_TO_DELETE/")

    override fun cleanUrl(url: String): String {
        if (url.contains("curseforge.com") && !url.endsWith("/download"))
            return "$url/download"

        return url
    }

    /**
     * Gets the forge version, can be based on the version from the downloaded pack
     *
     * @return String representation of the version
     */
    override fun getForgeVersion(): String {
        return forgeVersion
    }

    /**
     * Gets the forge version, can be based on the version from the downloaded pack
     *
     * @return String representation of the version
     */
    override fun getMCVersion(): String {
        return mcVersion
    }

    @Throws(IOException::class)
    override fun handleZip(file: File, pathMatchers: List<PathMatcher>) {
        // delete old installer folder
        FileUtils.deleteDirectory(oldFiles)

        // start with deleting the mods folder as it is not guaranteed to have override mods
        val modsFolder = File(basePath + "mods/")

        if (modsFolder.exists())
            FileUtils.moveDirectory(modsFolder, File(oldFiles, "mods"))
        LOGGER.info("Moved the mods folder")

        LOGGER.info("Starting to unzip files.")
        // unzip start
        try {
            ZipInputStream(FileInputStream(file)).use { zis ->
                var entry: ZipEntry? = zis.nextEntry

                loop@ while (entry != null) {
                    LOGGER.info("Entry in zip: $entry", true)
                    val name = entry.name

                    // special manifest treatment
                    if (name == "manifest.json")
                        zis.writeToFile(File(basePath + "manifest.json"))

                    // overrides
                    if (name.startsWith("overrides/")) {
                        val path = entry.name.substring(10)

                        when {
                            pathMatchers.any { it.matches(Paths.get(path)) } ->
                                LOGGER.info("Skipping $path as it is on the ignore List.", true)

                            !name.endsWith("/") -> {
                                val outfile = File(basePath + path)
                                LOGGER.info("Copying zip entry to = $outfile", true)

                                outfile.parentFile?.mkdirs()

                                zis.writeToFile(outfile)
                            }

                            name != "overrides/" -> {
                                val newFolder = File(basePath + path)
                                if (newFolder.exists())
                                    FileUtils.moveDirectory(newFolder, File(oldFiles, path))

                                LOGGER.info("Folder moved: " + newFolder.absolutePath, true)
                            }
                        }
                    }

                    entry = zis.nextEntry
                }

                zis.closeEntry()
            }
        } catch (e: IOException) {
            LOGGER.error("Could not unzip files", e)
        }

        LOGGER.info("Done unzipping the files.")
    }

    @Throws(IOException::class)
    override fun postProcessing() {
        val mods = ArrayList<ModEntryRaw>()

        InputStreamReader(FileInputStream(File(basePath + "manifest.json")), "utf-8").use { reader ->
            val json = JsonParser().parse(reader).asJsonObject
            LOGGER.info("manifest JSON Object: $json", true)
            val mcObj = json.getAsJsonObject("minecraft")

            if (mcVersion.isEmpty()) {
                mcVersion = mcObj.getAsJsonPrimitive("version").asString
            }

            // gets the forge version
            if (forgeVersion.isEmpty()) {
                val loaders = mcObj.getAsJsonArray("modLoaders")
                if (loaders.size() > 0) {
                    forgeVersion = loaders[0].asJsonObject.getAsJsonPrimitive("id").asString.substring(6)
                }
            }

            // gets all the mods
            for (jsonElement in json.getAsJsonArray("files")) {
                val obj = jsonElement.asJsonObject
                mods.add(ModEntryRaw(
                        obj.getAsJsonPrimitive("projectID").asString,
                        obj.getAsJsonPrimitive("fileID").asString))
            }
        }

        downloadMods(mods)
    }

    /**
     * Downloads the mods specified in the manifest
     * Gets the data from cursemeta
     *
     * @param mods List of the mods from the manifest
     */
    private fun downloadMods(mods: List<ModEntryRaw>) {
        val ignoreSet = HashSet<String>()
        val ignoreListTemp = configFile.install.getFormatSpecificSettingOrDefault<List<Any>>("ignoreProject", null)

        if (ignoreListTemp != null)
            for (o in ignoreListTemp) {
                if (o is String)
                    ignoreSet.add(o)

                if (o is Int)
                    ignoreSet.add(o.toString())
            }

        val urls = ConcurrentLinkedQueue<String>()

        LOGGER.info("Requesting Download links from cursemeta.")

        mods.parallelStream().forEach { mod ->
            if (ignoreSet.isNotEmpty() && ignoreSet.contains(mod.projectID)) {
                LOGGER.info("Skipping mod with projectID: " + mod.projectID)
                return@forEach
            }

            val url = "https://api.cfwidget.com/${mod.projectID}?version=${mod.fileID}"
            LOGGER.info("Download url is: $url", true)

            try {
                val request = Request.Builder()
                        .url(url)
                        .header("User-Agent", "All the mods server installer.")
                        .header("Content-Type", "application/json")
                        .build()

                val res = InternetManager.httpClient.newCall(request).execute()

                if (!res.isSuccessful)
                    throw IOException("Request to $url was not successful.")
                val body = res.body ?: throw IOException("Request to $url returned a null body.")
                try {
                    val jsonRes = JsonParser().parse(body.string()).asJsonObject
                    LOGGER.info("Response from manifest query: $jsonRes", true)
                    var arr = jsonRes.asJsonObject.getAsJsonObject("download");
                    var part1 = mod.fileID.subSequence(0, 4).toString();
                    var part2 = mod.fileID.split(part1)[1];
                    //         https://mediafilez.forgecdn.net/files/2743/38/useful_backpacks-1.16.5-1.12.1.90.jar
                    //         https://mediafilez.forgecdn.net/files/3350/860/useful_backpacks-1.16.5-1.12.1.90.jar
                    //         https://mediafilez.forgecdn.net/files/3871/353/MouseTweaks-forge-mc1.19-2.23.jar
                    //         https://mediafilez.forgecdn.net/files/3398//CosmeticArmorReworked-1.16.5-v4.jar
                    //         https://mediafilez.forgecdn.net/files/3398/0/CosmeticArmorReworked-1.16.5-v4.jar
                    //         https://mediafilez.forgecdn.net/files/3407/020/archers_paradox-1.16.5-1.3.1.jar
                    //         https://mediafilez.forgecdn.net/files/3407/20/archers_paradox-1.16.5-1.3.1.jar
                    val regx = "^0+\$".toRegex();
                    if(!regx.containsMatchIn(part2))
                        part2 = part2.replace("^0*".toRegex(),"");
                    else
                        part2 = "0";
                    var url = "https://mediafilez.forgecdn.net/files/$part1/$part2/${arr["name"].asString.replace("+","%2B")}"
                    LOGGER.info(url)
                    urls.add(url)
                }
                catch(e:Exception)
                {

                }

            } catch (e: IOException) {
                LOGGER.error("Error while trying to get URL from cursemeta for mod $mod", e)
            }
        }

        LOGGER.info("Mods to download: $urls", true)

        processMods(urls)

    }

    /**
     * Downloads all mods, with a second fallback if failed
     * This is done in parallel for better performance
     *
     * @param mods List of urls
     */
    private fun processMods(mods: Collection<String>) {
        // constructs the ignore list
        val ignorePatterns = ArrayList<Pattern>()
        for (ignoreFile in configFile.install.ignoreFiles) {
            if (ignoreFile.startsWith("mods/")) {
                ignorePatterns.add(Pattern.compile(ignoreFile.substring(ignoreFile.lastIndexOf('/'))))
            }
        }

        // downloads the mods
        val count = AtomicInteger(0)
        val totalCount = mods.size
        val fallbackList = ArrayList<String>()

        mods.stream().parallel().forEach { s -> processSingleMod(s, count, totalCount, fallbackList, ignorePatterns) }

        val secondFail = ArrayList<String>()
        fallbackList.forEach { s -> processSingleMod(s, count, totalCount, secondFail, ignorePatterns) }

        if (secondFail.isNotEmpty()) {
            LOGGER.warn("Failed to download (a) mod(s):")
            for (s in secondFail) {
                LOGGER.warn("\t" + s)
            }
        }
    }

    /**
     * Downloads a single mod and saves to the /mods directory
     *
     * @param mod            URL of the mod
     * @param counter        current counter of how many mods have already been downloaded
     * @param totalCount     total count of mods that have to be downloaded
     * @param fallbackList   List to write to when it failed
     * @param ignorePatterns Patterns of mods which should be ignored
     */
    private fun processSingleMod(mod: String, counter: AtomicInteger, totalCount: Int, fallbackList: MutableList<String>, ignorePatterns: List<Pattern>) {
        try {
            val modName = FilenameUtils.getName(mod)
            for (ignorePattern in ignorePatterns) {
                if (ignorePattern.matcher(modName).matches()) {
                    LOGGER.info("[" + counter.incrementAndGet() + "/" + totalCount + "] Skipped ignored mod: " + modName)
                }
            }

            InternetManager.downloadToFile(mod, File(basePath + "mods/" + modName))
            LOGGER.info("[" + String.format("% 3d", counter.incrementAndGet()) + "/" + totalCount + "] Downloaded mod: " + modName)

        } catch (e: IOException) {
            LOGGER.error("Failed to download mod", e)
            fallbackList.add(mod)

        } catch (e: URISyntaxException) {
            LOGGER.error("Invalid url for $mod", e)
        }
    }
}

/**
 * Data class to keep projectID and fileID together
 */
data class ModEntryRaw(val projectID: String, val fileID: String)