Initialer Commit

This commit is contained in:
2026-04-19 19:54:11 +02:00
commit afdda854d5
114 changed files with 424430 additions and 0 deletions

367
.classpath Normal file
View File

@@ -0,0 +1,367 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="bin/default"/>
<classpathentry output="bin/main" kind="src" path="src/main/java">
<attributes>
<attribute name="gradle_scope" value="main"/>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry output="bin/main" kind="src" path="src/main/resources">
<attributes>
<attribute name="gradle_scope" value="main"/>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17/"/>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-web/3.2.3/e91bf4f7d1f176e3809365df66b57b6920ce2e0a/spring-boot-starter-web-3.2.3-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-web/3.2.3/bf2b775d4f4e6349129c64de30939a5493779706/spring-boot-starter-web-3.2.3.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/com.google.code.gson/gson/2.10.1/982a90132e942f302e6fe79d6e78c4bc2e998569/gson-2.10.1-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/com.google.code.gson/gson/2.10.1/b3add478d4382b78ea20b1671390a858002feb6c/gson-2.10.1.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-json/3.2.3/7c44e81cf7a91455d41c1dcb1ca4dfaeacbfa4f9/spring-boot-starter-json-3.2.3-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-json/3.2.3/c71ec683425f09b7a213358d3b22959d929d1108/spring-boot-starter-json-3.2.3.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter/3.2.3/cc234d196c7403f924849a6db881f507b47caccb/spring-boot-starter-3.2.3-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter/3.2.3/15f3e03b0fd9570f90bdce9651610cc152534cf4/spring-boot-starter-3.2.3.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-tomcat/3.2.3/2e0beb1969719a944d2ffd39b64d188345abc70b/spring-boot-starter-tomcat-3.2.3-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-tomcat/3.2.3/ac392404d41766194b8fce6834c71879f9e8f479/spring-boot-starter-tomcat-3.2.3.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework/spring-webmvc/6.1.4/5312515a4e711152ab20cb53eefce85c885e1ff5/spring-webmvc-6.1.4-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework/spring-webmvc/6.1.4/e7aad7c53e05c8772209e915029e121262a7bc33/spring-webmvc-6.1.4.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework/spring-web/6.1.4/41a915620b58a36536852b7fb384804750df6ba/spring-web-6.1.4-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework/spring-web/6.1.4/b237532e03330a7cf8f66dc147e62bbbe44c702f/spring-web-6.1.4.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-autoconfigure/3.2.3/d89d997c7ae3b4fd0303b598c1dd20b8709f16a0/spring-boot-autoconfigure-3.2.3-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-autoconfigure/3.2.3/59db74eb4196885bb5a149417ab1ab51dc9b6952/spring-boot-autoconfigure-3.2.3.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot/3.2.3/6aafb01f8e3d3a968f6f3630c86f4acab9d6bf5/spring-boot-3.2.3-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot/3.2.3/b4aa6e3fdc4078fee4a4b9d702d9cc64e3fad1d4/spring-boot-3.2.3.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-logging/3.2.3/d89e2afa31fcdd4b4d3c65aa68ba7b93d7823cec/spring-boot-starter-logging-3.2.3-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-logging/3.2.3/c186015229d7f5cbd30ea99bf903a8cede6d849f/spring-boot-starter-logging-3.2.3.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/jakarta.annotation/jakarta.annotation-api/2.1.1/3beea3ed2e687d9bd8a78c00e18951fffccefe90/jakarta.annotation-api-2.1.1-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/jakarta.annotation/jakarta.annotation-api/2.1.1/48b9bda22b091b1f48b13af03fe36db3be6e1ae3/jakarta.annotation-api-2.1.1.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework/spring-context/6.1.4/b926271c28c9a70beadfcafd210b0fca465d5351/spring-context-6.1.4-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework/spring-context/6.1.4/765316bef55e41e4523f9b2780b8721ce5dd0fe2/spring-context-6.1.4.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework/spring-aop/6.1.4/9c23baecaa4dcf4cd52237d1935e96943ce671a9/spring-aop-6.1.4-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework/spring-aop/6.1.4/26ae2c9e7f69b0235a2faca1c58416c51eaebef6/spring-aop-6.1.4.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework/spring-beans/6.1.4/499ccf59eedb263d50d91cc00b9d34a5d70097f5/spring-beans-6.1.4-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework/spring-beans/6.1.4/e311cc9937d522a1051622580b4a2c250fc74164/spring-beans-6.1.4.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework/spring-expression/6.1.4/179e83febf6982faa42e48086df0b5f16bc0558f/spring-expression-6.1.4-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework/spring-expression/6.1.4/a1f2e3af83c7222b7f95f68a8e0666fdcceb35e1/spring-expression-6.1.4.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework/spring-core/6.1.4/a602755e3e97ef0bc40952636ba934433a71f61/spring-core-6.1.4-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework/spring-core/6.1.4/3b4dde8f55c3d5d4e948de64b866d7bb27e5a8d4/spring-core-6.1.4.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.yaml/snakeyaml/2.2/575a7915b847393207642575c139177ac2b2ac23/snakeyaml-2.2-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.yaml/snakeyaml/2.2/3af797a25458550a16bf89acc8e4ab2b7f2bfce0/snakeyaml-2.2.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.15.4/374791e1cb74eff66f761325affac3b3b2c8a1a1/jackson-datatype-jsr310-2.15.4-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.15.4/7de629770a4559db57128d35ccae7d2fddd35db3/jackson-datatype-jsr310-2.15.4.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.module/jackson-module-parameter-names/2.15.4/4c1d3cc9f6a69cbb03d3517085c92172ddb5fbd3/jackson-module-parameter-names-2.15.4-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.module/jackson-module-parameter-names/2.15.4/e654497a08359db2521b69b5f710e00836915d8c/jackson-module-parameter-names-2.15.4.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.core/jackson-annotations/2.15.4/704650e6ff1479d7847d61e9d578c1c5a9951f76/jackson-annotations-2.15.4-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.core/jackson-annotations/2.15.4/5223ea5a9bf52cdc9c5e537a0e52f2432eaf208b/jackson-annotations-2.15.4.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.core/jackson-core/2.15.4/1e96cdf3ba633b07b8d3cb514e9e5d899c8965f/jackson-core-2.15.4-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.core/jackson-core/2.15.4/aebe84b45360debad94f692a4074c6aceb535fa0/jackson-core-2.15.4.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.datatype/jackson-datatype-jdk8/2.15.4/1d5d9f86752c0c7d0d740e4cbb8b636b2cac5e5/jackson-datatype-jdk8-2.15.4-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.datatype/jackson-datatype-jdk8/2.15.4/694777f182334a21bf1aeab1b04cc4398c801f3f/jackson-datatype-jdk8-2.15.4.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.core/jackson-databind/2.15.4/b45d6e020c2dc43efac1fd1333a349eeafa9a8de/jackson-databind-2.15.4-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.core/jackson-databind/2.15.4/560309fc381f77d4d15c4a4cdaa0db5025c4fd13/jackson-databind-2.15.4.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.apache.tomcat.embed/tomcat-embed-websocket/10.1.19/949b0e6a2958c38d5e31c99a43520449cad06f7c/tomcat-embed-websocket-10.1.19-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.apache.tomcat.embed/tomcat-embed-websocket/10.1.19/adf4710fac2471236f8a466ca678cdf7e6a8257c/tomcat-embed-websocket-10.1.19.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.apache.tomcat.embed/tomcat-embed-core/10.1.19/be0e8644fe92a7333f59996ce214db7f893c9166/tomcat-embed-core-10.1.19-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.apache.tomcat.embed/tomcat-embed-core/10.1.19/3dbbca8acbd4dd6a137c3d6f934a2931512b42ce/tomcat-embed-core-10.1.19.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.apache.tomcat.embed/tomcat-embed-el/10.1.19/a4024787c4d3d5ca3f3176e0e14e8f6aa0a31db6/tomcat-embed-el-10.1.19-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.apache.tomcat.embed/tomcat-embed-el/10.1.19/c61a582c391aca130884a5421deedfe1a96c7415/tomcat-embed-el-10.1.19.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/io.micrometer/micrometer-observation/1.12.3/8aeac366ae3ac29958335b06801f163c06ec39d0/micrometer-observation-1.12.3-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/io.micrometer/micrometer-observation/1.12.3/105f768c211564fcebe4d79419bda4ebef0d0ac7/micrometer-observation-1.12.3.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-classic/1.4.14/1b0b0b96069e3ec3b4d8e5cbffc0a56e633063de/logback-classic-1.4.14-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-classic/1.4.14/d98bc162275134cdf1518774da4a2a17ef6fb94d/logback-classic-1.4.14.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.apache.logging.log4j/log4j-to-slf4j/2.21.1/31272b58321454b5d5a565f1e782ed4448ab3312/log4j-to-slf4j-2.21.1-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.apache.logging.log4j/log4j-to-slf4j/2.21.1/d77b2ba81711ed596cd797cc2b5b5bd7409d841c/log4j-to-slf4j-2.21.1.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.slf4j/jul-to-slf4j/2.0.12/7c0451bb352ce3f1ed9a0a35355a132ec08ba4c7/jul-to-slf4j-2.0.12-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.slf4j/jul-to-slf4j/2.0.12/eb5f48f782b41cc881b0bf1fb4d88ae2ff6d5b93/jul-to-slf4j-2.0.12.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework/spring-jcl/6.1.4/24e6bcbd5fb0d950142f7792df7b1ecfc5ecd5b0/spring-jcl-6.1.4-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework/spring-jcl/6.1.4/a244bd674a5431dfdce68d28cdf857104e6fff67/spring-jcl-6.1.4.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/io.micrometer/micrometer-commons/1.12.3/1da4cccb967f16fbd175ea2bbddd2985e01774d5/micrometer-commons-1.12.3-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/io.micrometer/micrometer-commons/1.12.3/83add2dda5cdc811fefb83e858c7412a176fe5b1/micrometer-commons-1.12.3.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-core/1.4.14/7d9dcce57ba091e1e4b4793b244508ea28f52b48/logback-core-1.4.14-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/ch.qos.logback/logback-core/1.4.14/4d3c2248219ac0effeb380ed4c5280a80bf395e8/logback-core-1.4.14.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-api/2.0.12/c2824e780c88689134cb99fb33c8a4ed3de0f406/slf4j-api-2.0.12-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.slf4j/slf4j-api/2.0.12/48f109a2a6d8f446c794f3e3fa0d86df0cdfa312/slf4j-api-2.0.12.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.apache.logging.log4j/log4j-api/2.21.1/7c87eb50e74855cd86f92bf370300b60ceef4a0b/log4j-api-2.21.1-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.apache.logging.log4j/log4j-api/2.21.1/74c65e87b9ce1694a01524e192d7be989ba70486/log4j-api-2.21.1.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-test/3.2.3/a4546d463e3f68794a061cc2fd01769bdd088d33/spring-boot-starter-test-3.2.3-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-starter-test/3.2.3/edfce43fbd303d556f3951af65dfb6e335d8230e/spring-boot-starter-test-3.2.3.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-test-autoconfigure/3.2.3/e8d56afca760da4184b74a93fd7a30e753d728f6/spring-boot-test-autoconfigure-3.2.3-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-test-autoconfigure/3.2.3/ea2e1778142fc8a05bd9325b433b13fe0a6845c1/spring-boot-test-autoconfigure-3.2.3.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-test/3.2.3/74a985e247a2655d8fe156dcd28fd3199ae3ddcd/spring-boot-test-3.2.3-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-test/3.2.3/4f440305c921caa9eb11624c1c6fa8fcc5200a64/spring-boot-test-3.2.3.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/com.jayway.jsonpath/json-path/2.9.0/a1ae84a6b97263c7ea566dfa73d5ac2271743e44/json-path-2.9.0-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/com.jayway.jsonpath/json-path/2.9.0/37fe2217f577b0b68b18e62c4d17a8858ecf9b69/json-path-2.9.0.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/jakarta.xml.bind/jakarta.xml.bind-api/4.0.1/7723d0945b45888b9d8af6e0358a5f5d5cdec2fd/jakarta.xml.bind-api-4.0.1-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/jakarta.xml.bind/jakarta.xml.bind-api/4.0.1/ca2330866cbc624c7e5ce982e121db1125d23e15/jakarta.xml.bind-api-4.0.1.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/net.minidev/json-smart/2.5.0/7f51740e5d2aa84831af62084d5ae98a51fcb61d/json-smart-2.5.0-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/net.minidev/json-smart/2.5.0/57a64f421b472849c40e77d2e7cce3a141b41e99/json-smart-2.5.0.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.assertj/assertj-core/3.24.2/76c425ccc878f91e347fa3cb0d686772679b66e2/assertj-core-3.24.2-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.assertj/assertj-core/3.24.2/ebbf338e33f893139459ce5df023115971c2786f/assertj-core-3.24.2.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.awaitility/awaitility/4.2.0/359e52d6a80b3d12fb08b0e665f15ce334d2c7a3/awaitility-4.2.0-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.awaitility/awaitility/4.2.0/2c39784846001a9cffd6c6b89c78de62c0d80fb8/awaitility-4.2.0.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.hamcrest/hamcrest/2.2/a0a13cfc629420efb587d954f982c4c6a100da25/hamcrest-2.2-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.hamcrest/hamcrest/2.2/1820c0968dba3a11a1b30669bb1f01978a91dedc/hamcrest-2.2.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.junit.jupiter/junit-jupiter-params/5.10.2/1add8e4310a408675f46eae8acafe6b2b72232e5/junit-jupiter-params-5.10.2-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.junit.jupiter/junit-jupiter-params/5.10.2/359132c82a9d3fa87a325db6edd33b5fdc67a3d8/junit-jupiter-params-5.10.2.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.junit.jupiter/junit-jupiter-api/5.10.2/b281dbb5921ce56b37b6e07a2502f663ca6c8db6/junit-jupiter-api-5.10.2-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.junit.jupiter/junit-jupiter-api/5.10.2/fb55d6e2bce173f35fd28422e7975539621055ef/junit-jupiter-api-5.10.2.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.junit.platform/junit-platform-commons/1.10.2/513097c1d045c39db2fa42a54f830d4358936dc3/junit-platform-commons-1.10.2-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.junit.platform/junit-platform-commons/1.10.2/3197154a1f0c88da46c47a9ca27611ac7ec5d797/junit-platform-commons-1.10.2.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.junit.jupiter/junit-jupiter/5.10.2/4e79ee610aff59d79b5f229252ded392b7d48fd1/junit-jupiter-5.10.2-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.junit.jupiter/junit-jupiter/5.10.2/831c0b86ddc2ce38391c5b81ea662b0cfdc02cce/junit-jupiter-5.10.2.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.mockito/mockito-junit-jupiter/5.7.0/5d3de2c05dc223dbb76d5d57125e529fe3623fec/mockito-junit-jupiter-5.7.0-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.mockito/mockito-junit-jupiter/5.7.0/ac2d6a3431747a7986b8f4abef465f72bf3a21ae/mockito-junit-jupiter-5.7.0.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.mockito/mockito-core/5.7.0/1baa9a13c3471920b3319e4d0389a4745fd2ab08/mockito-core-5.7.0-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.mockito/mockito-core/5.7.0/a1c258331ab91d66863c983aff7136357e9de056/mockito-core-5.7.0.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.skyscreamer/jsonassert/1.5.1/56cfa73a7ab13fbb8d433570add90f087d40e243/jsonassert-1.5.1-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.skyscreamer/jsonassert/1.5.1/6d842d0faf4cf6725c509a5e5347d319ee0431c3/jsonassert-1.5.1.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework/spring-test/6.1.4/57f1ccb91ff6a29de8cf394cdd1e8cf603da0ba7/spring-test-6.1.4-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.springframework/spring-test/6.1.4/acef358552d3bb56a4da0894b8d2277de8ae39d9/spring-test-6.1.4.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.xmlunit/xmlunit-core/2.9.1/8ef88e77c158cdc17de55ad94e1e7e7972a91bd6/xmlunit-core-2.9.1-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.xmlunit/xmlunit-core/2.9.1/e5833662d9a1279a37da3ef6f62a1da29fcd68c4/xmlunit-core-2.9.1.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/jakarta.activation/jakarta.activation-api/2.1.2/d0310cf32e4c43f65b942c18cb2cc2bccfe2de37/jakarta.activation-api-2.1.2-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/jakarta.activation/jakarta.activation-api/2.1.2/640c0d5aff45dbff1e1a1bc09673ff3a02b1ba12/jakarta.activation-api-2.1.2.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/net.minidev/accessors-smart/2.5.0/596cf1b2ac57a905102db8fb648c4080529f347f/accessors-smart-2.5.0-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/net.minidev/accessors-smart/2.5.0/aca011492dfe9c26f4e0659028a4fe0970829dd8/accessors-smart-2.5.0.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/net.bytebuddy/byte-buddy/1.14.12/887a4881279830607a6179560b17a44d62fc1997/byte-buddy-1.14.12-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/net.bytebuddy/byte-buddy/1.14.12/6e37f743dc15a8d7a4feb3eb0025cbc612d5b9e1/byte-buddy-1.14.12.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/net.bytebuddy/byte-buddy-agent/1.14.12/c9f6bb63cb973888eb2b2f139c6bfe5f3cc46ea3/byte-buddy-agent-1.14.12-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/net.bytebuddy/byte-buddy-agent/1.14.12/be4984cb6fd1ef1d11f218a648889dfda44b8a15/byte-buddy-agent-1.14.12.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/com.vaadin.external.google/android-json/0.0.20131108.vaadin1/bf42d7e47a3228513b626dd7d37ac6f072aeca4f/android-json-0.0.20131108.vaadin1-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/com.vaadin.external.google/android-json/0.0.20131108.vaadin1/fa26d351fe62a6a17f5cda1287c1c6110dec413f/android-json-0.0.20131108.vaadin1.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.ow2.asm/asm/9.3/ce26e415ccafa10c6624a07bdb38d969110f568f/asm-9.3-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.ow2.asm/asm/9.3/8e6300ef51c1d801a7ed62d07cd221aca3a90640/asm-9.3.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.opentest4j/opentest4j/1.3.0/afb8ff23cffb021c56f333953aebfe6e8818568e/opentest4j-1.3.0-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.opentest4j/opentest4j/1.3.0/152ea56b3a72f655d4fd677fc0ef2596c3dd5e6e/opentest4j-1.3.0.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.apiguardian/apiguardian-api/1.1.2/e0787a997746ac32639e0bf3cb27af8dce8a3428/apiguardian-api-1.1.2-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.apiguardian/apiguardian-api/1.1.2/a231e0d844d2721b0fa1b238006d15c6ded6842a/apiguardian-api-1.1.2.jar">
<attributes>
<attribute name="gradle_used_by_scope" value=""/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.junit.jupiter/junit-jupiter-engine/5.10.2/624f71a8d76185f2ac8d234c686bbdab0bd28ae0/junit-jupiter-engine-5.10.2-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.junit.jupiter/junit-jupiter-engine/5.10.2/f1f8fe97bd58e85569205f071274d459c2c4f8cd/junit-jupiter-engine-5.10.2.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.junit.platform/junit-platform-engine/1.10.2/886c197f5fcfe9eaf0d1dde20aa1bd3abd653f44/junit-platform-engine-1.10.2-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.junit.platform/junit-platform-engine/1.10.2/d53bb4e0ce7f211a498705783440614bfaf0df2e/junit-platform-engine-1.10.2.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry sourcepath="/home/mario/.gradle/caches/modules-2/files-2.1/org.objenesis/objenesis/3.3/5fef34eeee6816b0ba2170755a8a9db7744990c3/objenesis-3.3-sources.jar" kind="lib" path="/home/mario/.gradle/caches/modules-2/files-2.1/org.objenesis/objenesis/3.3/1049c09f1de4331e8193e579448d0916d75b7631/objenesis-3.3.jar">
<attributes>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
</classpath>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

Binary file not shown.

View File

@@ -0,0 +1,2 @@
#Sat Apr 18 12:25:04 CEST 2026
gradle.version=8.6

Binary file not shown.

BIN
.gradle/file-system.probe Normal file

Binary file not shown.

View File

26
.project Normal file
View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>linkster-backend</name>
<comment></comment>
<projects/>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments/>
</buildCommand>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments/>
</buildCommand>
<buildCommand>
<name>org.springframework.ide.eclipse.boot.validation.springbootbuilder</name>
<arguments/>
</buildCommand>
</buildSpec>
<linkedResources/>
<filteredResources/>
</projectDescription>

View File

@@ -0,0 +1,13 @@
arguments=
auto.sync=false
build.scans.enabled=false
connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
connection.project.dir=
eclipse.preferences.version=1
gradle.user.home=
java.home=
jvm.arguments=
offline.mode=false
override.workspace.settings=false
show.console.view=false
show.executions.view=false

View File

@@ -0,0 +1,8 @@
#
#Sat Apr 18 12:29:07 CEST 2026
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.targetPlatform=17
org.eclipse.jdt.core.compiler.compliance=17
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.source=17

View File

@@ -0,0 +1,2 @@
boot.validation.initialized=true
eclipse.preferences.version=1

4
Dockerfile Normal file
View File

@@ -0,0 +1,4 @@
FROM eclipse-temurin:17-jre-jammy
WORKDIR /app
COPY build/libs/*.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

View File

@@ -0,0 +1,2 @@
server.port=8090
spring.application.name=linkster-backend

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,35 @@
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE_ERRORS" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/errors.log</file>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>WARN</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/errors.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="de.oaa.linkster" level="INFO"/>
<root level="WARN">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE_ERRORS"/>
</root>
<logger name="de.oaa.linkster" level="INFO" additivity="false">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE_ERRORS"/>
</logger>
</configuration>

256
bin/main/playlists/all.json Normal file
View File

@@ -0,0 +1,256 @@
[
{
"id": "hitster-bingo-deutschland",
"name": "Hitster Bingo Deutschland",
"matcher": "/de/aaaa0019/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "full",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-ca",
"name": "Hitster Canada",
"matcher": "/ca/aaad0001/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-ca-franco",
"name": "Hitster Canada (Franco)",
"matcher": "/ca/aaad0002/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "no",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-ca-guilty-pleasures",
"name": "Hitster Guilty Pleasures (CA)",
"matcher": "/ca/aaad0003/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "full",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-de",
"name": "Hitster Deutsch",
"matcher": "/de/(\\d{5})$/",
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "full",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
},
"available": true
},
{
"id": "hitster-de-bayern-1-expansion",
"name": "Hitster Bayern 1 Expansion (DE)",
"matcher": "/de/aaaa0025/(\\d{5})$/",
"support": {
"appleMusic": "full",
"amazonMusic": "no",
"deezer": "full",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
},
"available": true
},
{
"id": "hitster-de-celebration",
"name": "Hitster Celebration (DE)",
"matcher": "/de/aaaa0040/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "no",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-de-guilty-pleasures",
"name": "Hitster Guilty Pleasures (DE)",
"matcher": "/de/aaaa0015/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-de-rock",
"name": "Hitster Rock (DE)",
"matcher": "/de/aaaa0039/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "full",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-de-schlager-party",
"name": "Hitster Schlager Party (DE)",
"matcher": "/de/aaaa0007/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "no",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-de-soundtracks-expansion",
"name": "Hitster Soundtracks Expansion (DE)",
"matcher": "/de/aaaa0026/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-de-summer-party",
"name": "Hitster Summer Party (DE)",
"matcher": "/de/aaaa0012/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-guilty-pleasures-nor",
"name": "Hitster Guilty Pleasures (NOR)",
"matcher": "/dk/aaaa0024/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "full",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-it",
"name": "Hitster Italia",
"matcher": "/it/aaac0001/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-uk",
"name": "Hitster UK",
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "no",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "no",
"youtubeMusic": "no"
},
"available": false
},
{
"id": "hitster-usa",
"name": "Hitster USA",
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "no",
"youtubeMusic": "no"
},
"available": false
}
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

359
bin/main/static/index.html Normal file
View File

@@ -0,0 +1,359 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Linkster</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #0f1117;
color: #e2e8f0;
line-height: 1.6;
padding: 2rem 1rem;
}
.container { max-width: 780px; margin: 0 auto; }
header { margin-bottom: 2.5rem; }
header h1 { font-size: 2rem; font-weight: 700; color: #f8fafc; }
header p { color: #94a3b8; margin-top: .4rem; }
h2 {
font-size: 1.1rem;
font-weight: 600;
color: #f8fafc;
margin: 2rem 0 .75rem;
padding-bottom: .4rem;
border-bottom: 1px solid #1e293b;
}
.endpoint {
background: #1e293b;
border: 1px solid #334155;
border-radius: .5rem;
padding: 1rem 1.25rem;
margin-bottom: 1rem;
}
.endpoint .method {
display: inline-block;
background: #2563eb;
color: #fff;
font-size: .75rem;
font-weight: 700;
padding: .15rem .5rem;
border-radius: .25rem;
margin-right: .6rem;
vertical-align: middle;
}
.endpoint .path {
font-family: "SFMono-Regular", Consolas, monospace;
font-size: .95rem;
color: #7dd3fc;
}
.endpoint .desc { color: #94a3b8; margin-top: .4rem; font-size: .9rem; }
table {
width: 100%;
border-collapse: collapse;
font-size: .9rem;
}
th {
text-align: left;
padding: .5rem .75rem;
background: #1e293b;
color: #94a3b8;
font-weight: 600;
font-size: .8rem;
text-transform: uppercase;
letter-spacing: .05em;
}
td {
padding: .5rem .75rem;
border-top: 1px solid #1e293b;
}
td code {
font-family: "SFMono-Regular", Consolas, monospace;
background: #1e293b;
padding: .1rem .35rem;
border-radius: .25rem;
font-size: .85rem;
color: #7dd3fc;
}
tr:hover td { background: #0f1e30; }
pre {
background: #1e293b;
border: 1px solid #334155;
border-radius: .5rem;
padding: 1rem 1.25rem;
overflow-x: auto;
font-family: "SFMono-Regular", Consolas, monospace;
font-size: .85rem;
line-height: 1.7;
}
.key { color: #7dd3fc; }
.str { color: #86efac; }
.bool { color: #f472b6; }
.badge {
display: inline-block;
font-size: .7rem;
padding: .1rem .4rem;
border-radius: .2rem;
font-weight: 600;
margin-left: .3rem;
}
.badge-full { background: #14532d; color: #86efac; }
.badge-no { background: #450a0a; color: #fca5a5; }
.playlist-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: .5rem;
}
.playlist-item {
background: #1e293b;
border: 1px solid #334155;
border-radius: .375rem;
padding: .5rem .75rem;
font-size: .85rem;
color: #cbd5e1;
}
footer {
margin-top: 3rem;
padding-top: 1rem;
border-top: 1px solid #1e293b;
color: #475569;
font-size: .8rem;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: .5rem;
}
footer a { color: #64748b; text-decoration: none; }
footer a:hover { color: #94a3b8; }
.download-btn {
display: inline-flex;
align-items: center;
gap: .5rem;
margin-top: .75rem;
padding: .5rem 1rem;
background: #16a34a;
color: #fff;
font-size: .85rem;
font-weight: 600;
border-radius: .375rem;
text-decoration: none;
}
.download-btn:hover { background: #15803d; }
.legal {
margin-top: 4rem;
padding-top: 2rem;
border-top: 2px solid #1e293b;
}
.legal h2 {
font-size: 1.3rem;
margin-top: 2.5rem;
}
.legal h3 {
font-size: .95rem;
font-weight: 600;
color: #cbd5e1;
margin: 1.25rem 0 .4rem;
}
.legal p {
color: #94a3b8;
font-size: .9rem;
margin-bottom: .6rem;
}
.legal address {
font-style: normal;
color: #94a3b8;
font-size: .9rem;
line-height: 1.8;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Linkster App</h1>
<p style="margin-top:.75rem">Löst die QR-Code aus einem bekannten Spiel auf und ermöglicht den Aufruf anderer Streaming Dienste als Spotify.</p>
<a class="download-btn" href="/linkster.apk" download="linkster-1.0.apk">
&#8595; Android App herunterladen (v1.0)
</a>
<p>
<h2>Installation</h2>
<h3>1. Die APK-Datei finden</h3>
<p>Zuerst musst du die Datei auf deinem Gerät finden. Meistens landet sie im Ordner Downloads.</p>
<p>Öffne eine Dateimanager-App (oft heißt sie "Eigene Dateien", "Files" oder "Dateien").</p>
<p>Navigiere zum Ordner Downloads und tippe die APK-Datei an.</p>
<h3>2. Installation aus unbekannten Quellen erlauben</h3>
<p>Wenn du die Datei das erste Mal antippst, erscheint normalerweise eine Sicherheitswarnung.</p>
<p>Tippe in dem Pop-up-Fenster auf Einstellungen.</p>
<p>Du wirst nun zu einem Menü weitergeleitet ("Unbekannte Apps installieren"). Dort musst du den Schalter bei der App aktivieren, mit der du die APK öffnen willst (z. B. Chrome oder dein Dateimanager).</p>
<p>Gehe danach zurück zur Datei.</p>
<h3>3. Installation durchführen</h3>
<p>Tippe die Datei erneut an.</p>
<p>Es erscheint die Frage: "Möchtest du diese App installieren?". Bestätige dies mit Installieren.</p>
<p>Nach kurzem Warten ist die App bereit und du kannst auf Öffnen tippen.</p>
</header>
<h1>Linkster API</h1>
<h2>Endpunkte</h2>
<div class="endpoint">
<span class="method">GET</span>
<span class="path">/api/resolve?url={url}</span>
<div class="desc">Löst einen Streaming-Link auf und gibt Titel, Künstler und verfügbare Provider-Links zurück.</div>
</div>
<div class="endpoint">
<span class="method">GET</span>
<span class="path">/api/health</span>
<div class="desc">Gibt <code>"ok"</code> zurück, wenn der Server läuft.</div>
</div>
<h2>Parameter</h2>
<table>
<thead>
<tr><th>Parameter</th><th>Typ</th><th>Beschreibung</th></tr>
</thead>
<tbody>
<tr>
<td><code>url</code></td>
<td>string</td>
<td>Die vollständige URL vom Hitster-QR-Code (z.B. Apple Music Link)</td>
</tr>
</tbody>
</table>
<h2>Antwort (gefunden)</h2>
<pre><span class="key">"found"</span>: <span class="bool">true</span>,
<span class="key">"title"</span>: <span class="str">"Song-Titel"</span>,
<span class="key">"artist"</span>: <span class="str">"Künstlername"</span>,
<span class="key">"playlist"</span>: <span class="str">"Hitster Deutsch"</span>,
<span class="key">"providers"</span>: {
<span class="key">"spotify"</span>: <span class="str">"https://open.spotify.com/track/..."</span>,
<span class="key">"deezer"</span>: <span class="str">"https://www.deezer.com/track/..."</span>,
<span class="key">"youtube"</span>: <span class="str">"https://www.youtube.com/watch?v=..."</span>,
<span class="key">"youtubeMusic"</span>: <span class="str">"https://music.youtube.com/watch?v=..."</span>,
<span class="key">"appleMusic"</span>: <span class="str">"https://music.apple.com/..."</span>,
<span class="key">"amazonMusic"</span>: <span class="str">"https://music.amazon.com/..."</span>,
<span class="key">"tidal"</span>: <span class="str">"https://tidal.com/track/..."</span>
}</pre>
<h2>Antwort (nicht gefunden)</h2>
<pre><span class="key">"found"</span>: <span class="bool">false</span></pre>
<h2>Unterstützte Playlists</h2>
<div class="playlist-grid">
<div class="playlist-item">Hitster Deutsch</div>
<div class="playlist-item">Hitster Bingo Deutschland</div>
<div class="playlist-item">Hitster Bayern 1 Expansion (DE)</div>
<div class="playlist-item">Hitster Celebration (DE)</div>
<div class="playlist-item">Hitster Guilty Pleasures (DE)</div>
<div class="playlist-item">Hitster Rock (DE)</div>
<div class="playlist-item">Hitster Schlager Party (DE)</div>
<div class="playlist-item">Hitster Soundtracks Expansion (DE)</div>
<div class="playlist-item">Hitster Summer Party (DE)</div>
<div class="playlist-item">Hitster Canada</div>
<div class="playlist-item">Hitster Canada (Franco)</div>
<div class="playlist-item">Hitster Guilty Pleasures (CA)</div>
<div class="playlist-item">Hitster Guilty Pleasures (NOR)</div>
<div class="playlist-item">Hitster Italia</div>
</div>
<footer>
<span>Linkster &mdash; linkster.langhei.de</span>
<span>
<a href="#impressum">Impressum</a>
&ensp;&middot;&ensp;
<a href="#datenschutz">Datenschutz</a>
</span>
</footer>
<div class="legal">
<h2 id="impressum">Impressum</h2>
<h3>Angaben gemäß § 5 TMG</h3>
<address>
Mario Störmer<br>
Langheide 14<br>
24354 Rieseby<br>
Deutschland
</address>
<h3>Hinweis</h3>
<p>
Dieses Angebot ist ein privates, nicht-kommerzielles Hobbyprojekt.
Es werden keine wirtschaftlichen Interessen verfolgt und kein Entgelt erhoben.
</p>
<h2 id="datenschutz">Datenschutzerklärung</h2>
<h3>Verantwortlicher</h3>
<address>
Mario Störmer<br>
Langheide 14<br>
24354 Rieseby<br>
Deutschland
</address>
<h3>Welche Daten werden verarbeitet?</h3>
<p>
Beim Aufruf dieser Seite und der API werden technisch bedingt folgende Daten
kurzfristig im Server-Log gespeichert:
</p>
<p>
IP-Adresse des anfragenden Geräts, aufgerufene URL, Zeitpunkt der Anfrage sowie
HTTP-Statuscode. Diese Daten sind für den stabilen Betrieb des Dienstes notwendig
und werden nicht an Dritte weitergegeben.
</p>
<h3>Zweck der Verarbeitung</h3>
<p>
Die Daten werden ausschließlich zur Fehleranalyse und Sicherstellung des Betriebs
verwendet. Eine Auswertung zu Analyse-, Tracking- oder Werbezwecken findet nicht statt.
</p>
<h3>Speicherdauer</h3>
<p>
Server-Logs werden automatisch nach 30 Tagen gelöscht. Es erfolgt keine
dauerhafte Speicherung personenbezogener Daten.
</p>
<h3>Cookies &amp; Tracking</h3>
<p>
Diese Website verwendet keine Cookies, keine Analyse-Tools und kein Tracking
jeglicher Art.
</p>
<h3>Weitergabe an Dritte</h3>
<p>
Es findet keine Weitergabe von Daten an Dritte statt. Der Dienst ist
ausschließlich für den privaten Gebrauch bestimmt.
</p>
<h3>Rechte betroffener Personen</h3>
<p>
Gemäß DSGVO stehen Ihnen das Recht auf Auskunft, Berichtigung, Löschung und
Einschränkung der Verarbeitung zu. Anfragen können per Post an die oben genannte
Adresse gerichtet werden.
</p>
</div>
</div>
</body>
</html>

26
build.gradle Normal file
View File

@@ -0,0 +1,26 @@
plugins {
id 'org.springframework.boot' version '3.2.3'
id 'io.spring.dependency-management' version '1.1.4'
id 'java'
}
group = 'de.oaa.linkster'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'com.google.code.gson:gson:2.10.1'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
bootBuildImage {
imageName = 'linkster-backend:latest'
}

Binary file not shown.

View File

@@ -0,0 +1 @@
de.oaa.linkster.LinksterApplication

View File

@@ -0,0 +1,2 @@
server.port=8090
spring.application.name=linkster-backend

View File

@@ -0,0 +1,35 @@
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE_ERRORS" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/errors.log</file>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>WARN</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/errors.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="de.oaa.linkster" level="INFO"/>
<root level="WARN">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE_ERRORS"/>
</root>
<logger name="de.oaa.linkster" level="INFO" additivity="false">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE_ERRORS"/>
</logger>
</configuration>

View File

@@ -0,0 +1,256 @@
[
{
"id": "hitster-bingo-deutschland",
"name": "Hitster Bingo Deutschland",
"matcher": "/de/aaaa0019/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "full",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-ca",
"name": "Hitster Canada",
"matcher": "/ca/aaad0001/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-ca-franco",
"name": "Hitster Canada (Franco)",
"matcher": "/ca/aaad0002/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "no",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-ca-guilty-pleasures",
"name": "Hitster Guilty Pleasures (CA)",
"matcher": "/ca/aaad0003/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "full",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-de",
"name": "Hitster Deutsch",
"matcher": "/de/(\\d{5})$/",
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "full",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
},
"available": true
},
{
"id": "hitster-de-bayern-1-expansion",
"name": "Hitster Bayern 1 Expansion (DE)",
"matcher": "/de/aaaa0025/(\\d{5})$/",
"support": {
"appleMusic": "full",
"amazonMusic": "no",
"deezer": "full",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
},
"available": true
},
{
"id": "hitster-de-celebration",
"name": "Hitster Celebration (DE)",
"matcher": "/de/aaaa0040/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "no",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-de-guilty-pleasures",
"name": "Hitster Guilty Pleasures (DE)",
"matcher": "/de/aaaa0015/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-de-rock",
"name": "Hitster Rock (DE)",
"matcher": "/de/aaaa0039/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "full",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-de-schlager-party",
"name": "Hitster Schlager Party (DE)",
"matcher": "/de/aaaa0007/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "no",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-de-soundtracks-expansion",
"name": "Hitster Soundtracks Expansion (DE)",
"matcher": "/de/aaaa0026/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-de-summer-party",
"name": "Hitster Summer Party (DE)",
"matcher": "/de/aaaa0012/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-guilty-pleasures-nor",
"name": "Hitster Guilty Pleasures (NOR)",
"matcher": "/dk/aaaa0024/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "full",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-it",
"name": "Hitster Italia",
"matcher": "/it/aaac0001/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-uk",
"name": "Hitster UK",
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "no",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "no",
"youtubeMusic": "no"
},
"available": false
},
{
"id": "hitster-usa",
"name": "Hitster USA",
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "no",
"youtubeMusic": "no"
},
"available": false
}
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

View File

@@ -0,0 +1,361 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Linkster</title>
<link rel="icon" type="image/png" href="/favicon.png">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #0f1117;
color: #e2e8f0;
line-height: 1.6;
padding: 2rem 1rem;
}
.container { max-width: 780px; margin: 0 auto; }
header { margin-bottom: 2.5rem; }
header h1 { font-size: 2rem; font-weight: 700; color: #f8fafc; }
header p { color: #94a3b8; margin-top: .4rem; }
h2 {
font-size: 1.1rem;
font-weight: 600;
color: #f8fafc;
margin: 2rem 0 .75rem;
padding-bottom: .4rem;
border-bottom: 1px solid #1e293b;
}
.endpoint {
background: #1e293b;
border: 1px solid #334155;
border-radius: .5rem;
padding: 1rem 1.25rem;
margin-bottom: 1rem;
}
.endpoint .method {
display: inline-block;
background: #2563eb;
color: #fff;
font-size: .75rem;
font-weight: 700;
padding: .15rem .5rem;
border-radius: .25rem;
margin-right: .6rem;
vertical-align: middle;
}
.endpoint .path {
font-family: "SFMono-Regular", Consolas, monospace;
font-size: .95rem;
color: #7dd3fc;
}
.endpoint .desc { color: #94a3b8; margin-top: .4rem; font-size: .9rem; }
table {
width: 100%;
border-collapse: collapse;
font-size: .9rem;
}
th {
text-align: left;
padding: .5rem .75rem;
background: #1e293b;
color: #94a3b8;
font-weight: 600;
font-size: .8rem;
text-transform: uppercase;
letter-spacing: .05em;
}
td {
padding: .5rem .75rem;
border-top: 1px solid #1e293b;
}
td code {
font-family: "SFMono-Regular", Consolas, monospace;
background: #1e293b;
padding: .1rem .35rem;
border-radius: .25rem;
font-size: .85rem;
color: #7dd3fc;
}
tr:hover td { background: #0f1e30; }
pre {
background: #1e293b;
border: 1px solid #334155;
border-radius: .5rem;
padding: 1rem 1.25rem;
overflow-x: auto;
font-family: "SFMono-Regular", Consolas, monospace;
font-size: .85rem;
line-height: 1.7;
}
.key { color: #7dd3fc; }
.str { color: #86efac; }
.bool { color: #f472b6; }
.badge {
display: inline-block;
font-size: .7rem;
padding: .1rem .4rem;
border-radius: .2rem;
font-weight: 600;
margin-left: .3rem;
}
.badge-full { background: #14532d; color: #86efac; }
.badge-no { background: #450a0a; color: #fca5a5; }
.playlist-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: .5rem;
}
.playlist-item {
background: #1e293b;
border: 1px solid #334155;
border-radius: .375rem;
padding: .5rem .75rem;
font-size: .85rem;
color: #cbd5e1;
}
footer {
margin-top: 3rem;
padding-top: 1rem;
border-top: 1px solid #1e293b;
color: #475569;
font-size: .8rem;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: .5rem;
}
footer a { color: #64748b; text-decoration: none; }
footer a:hover { color: #94a3b8; }
.download-btn {
display: inline-flex;
align-items: center;
gap: .5rem;
margin-top: .75rem;
padding: .5rem 1rem;
background: #16a34a;
color: #fff;
font-size: .85rem;
font-weight: 600;
border-radius: .375rem;
text-decoration: none;
}
.download-btn:hover { background: #15803d; }
.legal {
margin-top: 4rem;
padding-top: 2rem;
border-top: 2px solid #1e293b;
}
.legal h2 {
font-size: 1.3rem;
margin-top: 2.5rem;
}
.legal h3 {
font-size: .95rem;
font-weight: 600;
color: #cbd5e1;
margin: 1.25rem 0 .4rem;
}
.legal p {
color: #94a3b8;
font-size: .9rem;
margin-bottom: .6rem;
}
.legal address {
font-style: normal;
color: #94a3b8;
font-size: .9rem;
line-height: 1.8;
}
</style>
</head>
<body>
<div class="container">
<header>
<img src="/favicon.png" alt="Linkster Logo" style="width:80px;height:80px;border-radius:.75rem;margin-bottom:.75rem;display:block;">
<h1>Linkster App</h1>
<p style="margin-top:.75rem">Löst die QR-Code aus einem bekannten Spiel auf und ermöglicht den Aufruf anderer Streaming Dienste als Spotify.</p>
<a class="download-btn" href="/linkster.apk" download="linkster-1.0.apk">
&#8595; Android App herunterladen (v1.0)
</a>
<p>
<h2>Installation</h2>
<h3>1. Die APK-Datei finden</h3>
<p>Zuerst musst du die Datei auf deinem Gerät finden. Meistens landet sie im Ordner Downloads.</p>
<p>Öffne eine Dateimanager-App (oft heißt sie "Eigene Dateien", "Files" oder "Dateien").</p>
<p>Navigiere zum Ordner Downloads und tippe die APK-Datei an.</p>
<h3>2. Installation aus unbekannten Quellen erlauben</h3>
<p>Wenn du die Datei das erste Mal antippst, erscheint normalerweise eine Sicherheitswarnung.</p>
<p>Tippe in dem Pop-up-Fenster auf Einstellungen.</p>
<p>Du wirst nun zu einem Menü weitergeleitet ("Unbekannte Apps installieren"). Dort musst du den Schalter bei der App aktivieren, mit der du die APK öffnen willst (z. B. Chrome oder dein Dateimanager).</p>
<p>Gehe danach zurück zur Datei.</p>
<h3>3. Installation durchführen</h3>
<p>Tippe die Datei erneut an.</p>
<p>Es erscheint die Frage: "Möchtest du diese App installieren?". Bestätige dies mit Installieren.</p>
<p>Nach kurzem Warten ist die App bereit und du kannst auf Öffnen tippen.</p>
</header>
<h1>Linkster API</h1>
<h2>Endpunkte</h2>
<div class="endpoint">
<span class="method">GET</span>
<span class="path">/api/resolve?url={url}</span>
<div class="desc">Löst einen Streaming-Link auf und gibt Titel, Künstler und verfügbare Provider-Links zurück.</div>
</div>
<div class="endpoint">
<span class="method">GET</span>
<span class="path">/api/health</span>
<div class="desc">Gibt <code>"ok"</code> zurück, wenn der Server läuft.</div>
</div>
<h2>Parameter</h2>
<table>
<thead>
<tr><th>Parameter</th><th>Typ</th><th>Beschreibung</th></tr>
</thead>
<tbody>
<tr>
<td><code>url</code></td>
<td>string</td>
<td>Die vollständige URL vom Hitster-QR-Code (z.B. Apple Music Link)</td>
</tr>
</tbody>
</table>
<h2>Antwort (gefunden)</h2>
<pre><span class="key">"found"</span>: <span class="bool">true</span>,
<span class="key">"title"</span>: <span class="str">"Song-Titel"</span>,
<span class="key">"artist"</span>: <span class="str">"Künstlername"</span>,
<span class="key">"playlist"</span>: <span class="str">"Hitster Deutsch"</span>,
<span class="key">"providers"</span>: {
<span class="key">"spotify"</span>: <span class="str">"https://open.spotify.com/track/..."</span>,
<span class="key">"deezer"</span>: <span class="str">"https://www.deezer.com/track/..."</span>,
<span class="key">"youtube"</span>: <span class="str">"https://www.youtube.com/watch?v=..."</span>,
<span class="key">"youtubeMusic"</span>: <span class="str">"https://music.youtube.com/watch?v=..."</span>,
<span class="key">"appleMusic"</span>: <span class="str">"https://music.apple.com/..."</span>,
<span class="key">"amazonMusic"</span>: <span class="str">"https://music.amazon.com/..."</span>,
<span class="key">"tidal"</span>: <span class="str">"https://tidal.com/track/..."</span>
}</pre>
<h2>Antwort (nicht gefunden)</h2>
<pre><span class="key">"found"</span>: <span class="bool">false</span></pre>
<h2>Unterstützte Playlists</h2>
<div class="playlist-grid">
<div class="playlist-item">Hitster Deutsch</div>
<div class="playlist-item">Hitster Bingo Deutschland</div>
<div class="playlist-item">Hitster Bayern 1 Expansion (DE)</div>
<div class="playlist-item">Hitster Celebration (DE)</div>
<div class="playlist-item">Hitster Guilty Pleasures (DE)</div>
<div class="playlist-item">Hitster Rock (DE)</div>
<div class="playlist-item">Hitster Schlager Party (DE)</div>
<div class="playlist-item">Hitster Soundtracks Expansion (DE)</div>
<div class="playlist-item">Hitster Summer Party (DE)</div>
<div class="playlist-item">Hitster Canada</div>
<div class="playlist-item">Hitster Canada (Franco)</div>
<div class="playlist-item">Hitster Guilty Pleasures (CA)</div>
<div class="playlist-item">Hitster Guilty Pleasures (NOR)</div>
<div class="playlist-item">Hitster Italia</div>
</div>
<footer>
<span>Linkster &mdash; linkster.langhei.de</span>
<span>
<a href="#impressum">Impressum</a>
&ensp;&middot;&ensp;
<a href="#datenschutz">Datenschutz</a>
</span>
</footer>
<div class="legal">
<h2 id="impressum">Impressum</h2>
<h3>Angaben gemäß § 5 TMG</h3>
<address>
Mario Störmer<br>
Langheide 14<br>
24354 Rieseby<br>
Deutschland
</address>
<h3>Hinweis</h3>
<p>
Dieses Angebot ist ein privates, nicht-kommerzielles Hobbyprojekt.
Es werden keine wirtschaftlichen Interessen verfolgt und kein Entgelt erhoben.
</p>
<h2 id="datenschutz">Datenschutzerklärung</h2>
<h3>Verantwortlicher</h3>
<address>
Mario Störmer<br>
Langheide 14<br>
24354 Rieseby<br>
Deutschland
</address>
<h3>Welche Daten werden verarbeitet?</h3>
<p>
Beim Aufruf dieser Seite und der API werden technisch bedingt folgende Daten
kurzfristig im Server-Log gespeichert:
</p>
<p>
IP-Adresse des anfragenden Geräts, aufgerufene URL, Zeitpunkt der Anfrage sowie
HTTP-Statuscode. Diese Daten sind für den stabilen Betrieb des Dienstes notwendig
und werden nicht an Dritte weitergegeben.
</p>
<h3>Zweck der Verarbeitung</h3>
<p>
Die Daten werden ausschließlich zur Fehleranalyse und Sicherstellung des Betriebs
verwendet. Eine Auswertung zu Analyse-, Tracking- oder Werbezwecken findet nicht statt.
</p>
<h3>Speicherdauer</h3>
<p>
Server-Logs werden automatisch nach 30 Tagen gelöscht. Es erfolgt keine
dauerhafte Speicherung personenbezogener Daten.
</p>
<h3>Cookies &amp; Tracking</h3>
<p>
Diese Website verwendet keine Cookies, keine Analyse-Tools und kein Tracking
jeglicher Art.
</p>
<h3>Weitergabe an Dritte</h3>
<p>
Es findet keine Weitergabe von Daten an Dritte statt. Der Dienst ist
ausschließlich für den privaten Gebrauch bestimmt.
</p>
<h3>Rechte betroffener Personen</h3>
<p>
Gemäß DSGVO stehen Ihnen das Recht auf Auskunft, Berichtigung, Löschung und
Einschränkung der Verarbeitung zu. Anfragen können per Post an die oben genannte
Adresse gerichtet werden.
</p>
</div>
</div>
</body>
</html>

Binary file not shown.

View File

@@ -0,0 +1,12 @@
Manifest-Version: 1.0
Main-Class: org.springframework.boot.loader.launch.JarLauncher
Start-Class: de.oaa.linkster.LinksterApplication
Spring-Boot-Version: 3.2.3
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Build-Jdk-Spec: 17
Implementation-Title: linkster-backend
Implementation-Version: 0.0.1-SNAPSHOT

Binary file not shown.

19
deploy.sh Executable file
View File

@@ -0,0 +1,19 @@
#!/bin/bash
set -e
REMOTE_CONTEXT="proxmox-remote"
IMAGE_NAME="linkster-backend"
TAG="latest"
echo "--- 1. Gradle Build: Erstelle JAR und Docker Image lokal ---"
./gradlew bootJar
docker build -t $IMAGE_NAME:$TAG .
echo "--- 2. Transfer: Image zum Proxmox-Server schieben ---"
docker save $IMAGE_NAME:$TAG | docker --context $REMOTE_CONTEXT load
echo "--- 3. Remote Deployment: Starten auf Proxmox ---"
docker --context $REMOTE_CONTEXT compose up -d --force-recreate
echo "--- Fertig! Backend läuft auf Port 8090 ---"
echo "--- Erreichbar unter: https://linkster.langhei.de/api/ ---"

14
docker-compose.yml Normal file
View File

@@ -0,0 +1,14 @@
services:
linkster-backend:
image: linkster-backend:latest
container_name: linkster-backend
restart: unless-stopped
ports:
- "8090:8090"
environment:
- SPRING_PROFILES_ACTIVE=prod
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8090/api/health"]
interval: 30s
timeout: 5s
retries: 3

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

221
gradlew vendored Executable file
View File

@@ -0,0 +1,221 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by "Gradle init".
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other POSIX-compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for contributing:
#
# (2) This script should remain compatible with sh and target the lowest common
# denominator POSIX shell, even when local shells might be more capable.
# POSIX features are given preference over "smarts" that would make the
# script easier to write or maintain, or that might otherwise work on
# non-POSIX-compliant shells.
#
# (3) This script must be sourced from the root directory of the project, not
# from outside it.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #( absolute
*) app_path=$APP_HOME$link ;; #( relative
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
;;
esac
case $MAX_FD in #(
'' | soft) :;; #( No-op if MAX_FD is empty or 'soft'
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# temporary variables. (ref: https://unix.stackexchange.com/a/338087)
set -- "$@" "$arg"
shift
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
# shellcheck disable=SC2086
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# temporary variable that contains a space. They are then exported to the
# function, which does the final interpretation.
#
# NOTE: The sed script strips any leading and trailing whitespace as we
# don't want to add extra whitespace to the args.
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^a-zA-Z0-9/=@._-]~\\&~g; ' |
tr '\n' ' '
) $@"
exec "$JAVACMD" "$@"

1
settings.gradle Normal file
View File

@@ -0,0 +1 @@
rootProject.name = 'linkster-backend'

View File

@@ -0,0 +1,11 @@
package de.oaa.linkster;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class LinksterApplication {
public static void main(String[] args) {
SpringApplication.run(LinksterApplication.class, args);
}
}

View File

@@ -0,0 +1,31 @@
package de.oaa.linkster.controller;
import de.oaa.linkster.model.ResolveResponse;
import de.oaa.linkster.service.PlaylistService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class ResolveController {
private final PlaylistService playlistService;
public ResolveController(PlaylistService playlistService) {
this.playlistService = playlistService;
}
@GetMapping("/resolve")
public ResponseEntity<ResolveResponse> resolve(@RequestParam String url) {
ResolveResponse response = playlistService.resolve(url);
if (response.isFound()) {
return ResponseEntity.ok(response);
}
return ResponseEntity.ok(response); // 200 mit found=false, App entscheidet
}
@GetMapping("/health")
public ResponseEntity<String> health() {
return ResponseEntity.ok("ok");
}
}

View File

@@ -0,0 +1,42 @@
package de.oaa.linkster.filter;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class RequestLoggingFilter implements Filter {
private static final Logger log = LoggerFactory.getLogger(RequestLoggingFilter.class);
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
long start = System.currentTimeMillis();
try {
chain.doFilter(req, res);
} finally {
long ms = System.currentTimeMillis() - start;
String query = request.getQueryString();
String uri = query != null ? request.getRequestURI() + "?" + query : request.getRequestURI();
int status = response.getStatus();
if (status >= 500) {
log.error("{} {} {} {}ms", request.getMethod(), uri, status, ms);
} else if (status >= 400) {
log.warn("{} {} {} {}ms", request.getMethod(), uri, status, ms);
} else {
log.info("{} {} {} {}ms", request.getMethod(), uri, status, ms);
}
}
}
}

View File

@@ -0,0 +1,36 @@
package de.oaa.linkster.model;
import java.util.Map;
public class ResolveResponse {
private final boolean found;
private String title;
private String artist;
private String playlist;
private Map<String, String> providers;
private ResolveResponse(boolean found) {
this.found = found;
}
public static ResolveResponse notFound() {
return new ResolveResponse(false);
}
public static ResolveResponse of(String title, String artist, String playlist,
Map<String, String> providers) {
ResolveResponse r = new ResolveResponse(true);
r.title = title;
r.artist = artist;
r.playlist = playlist;
r.providers = providers;
return r;
}
public boolean isFound() { return found; }
public String getTitle() { return title; }
public String getArtist() { return artist; }
public String getPlaylist() { return playlist; }
public Map<String, String> getProviders() { return providers; }
}

View File

@@ -0,0 +1,119 @@
package de.oaa.linkster.service;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import de.oaa.linkster.model.ResolveResponse;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Service
public class PlaylistService {
private static final Logger log = LoggerFactory.getLogger(PlaylistService.class);
private record PlaylistEntry(String id, String name, Pattern matcher) {}
/** In-Memory-DB: playlistId → songId → SongData */
private final ConcurrentHashMap<String, ConcurrentHashMap<String, JsonObject>> db
= new ConcurrentHashMap<>();
private final List<PlaylistEntry> index = new ArrayList<>();
@PostConstruct
public void init() {
try {
JsonArray all = readJson("playlists/all.json").getAsJsonArray();
for (JsonElement elem : all) {
JsonObject obj = elem.getAsJsonObject();
if (!obj.has("matcher")) continue;
if (obj.has("available") && !obj.get("available").getAsBoolean()) continue;
String id = obj.get("id").getAsString();
String name = obj.get("name").getAsString();
String raw = obj.get("matcher").getAsString();
String pattern = raw.replaceAll("^/|/[a-z]*$", "");
try {
index.add(new PlaylistEntry(id, name, Pattern.compile(pattern)));
loadPlaylist(id);
} catch (Exception e) {
log.warn("Überspringe Playlist {}: {}", id, e.getMessage());
}
}
log.info("Linkster-DB bereit: {} Playlists geladen", db.size());
} catch (Exception e) {
throw new RuntimeException("Fehler beim Laden der Playlists", e);
}
}
private void loadPlaylist(String id) throws Exception {
JsonObject playlist = readJson("playlists/" + id + ".json").getAsJsonObject();
JsonObject songs = playlist.getAsJsonObject("songs");
ConcurrentHashMap<String, JsonObject> songMap = new ConcurrentHashMap<>();
for (Map.Entry<String, JsonElement> entry : songs.entrySet()) {
songMap.put(entry.getKey(), entry.getValue().getAsJsonObject());
}
db.put(id, songMap);
log.debug(" {} → {} Songs", id, songMap.size());
}
public ResolveResponse resolve(String url) {
for (PlaylistEntry entry : index) {
Matcher m = entry.matcher().matcher(url);
if (!m.find()) continue;
int songId = Integer.parseInt(m.group(1));
ConcurrentHashMap<String, JsonObject> songs = db.get(entry.id());
if (songs == null) continue;
JsonObject song = songs.get(String.valueOf(songId));
if (song == null) continue;
String title = song.get("title").getAsString();
String artist = song.get("artistName").getAsString();
Map<String, String> providers = extractProviders(song);
return ResolveResponse.of(title, artist, entry.name(), providers);
}
return ResolveResponse.notFound();
}
private Map<String, String> extractProviders(JsonObject song) {
Map<String, String> result = new LinkedHashMap<>();
JsonObject providers = song.getAsJsonObject("providers");
if (providers == null) return result;
for (Map.Entry<String, JsonElement> p : providers.entrySet()) {
JsonObject regions = p.getValue().getAsJsonObject();
for (Map.Entry<String, JsonElement> r : regions.entrySet()) {
String url = r.getValue().getAsString();
if (url != null && !url.isEmpty()) {
result.put(p.getKey(), url);
break;
}
}
}
return result;
}
private JsonElement readJson(String path) throws Exception {
try (var is = new ClassPathResource(path).getInputStream();
var reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
return JsonParser.parseReader(reader);
}
}
}

View File

@@ -0,0 +1,2 @@
server.port=8090
spring.application.name=linkster-backend

View File

@@ -0,0 +1,35 @@
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE_ERRORS" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/errors.log</file>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>WARN</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/errors.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="de.oaa.linkster" level="INFO"/>
<root level="WARN">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE_ERRORS"/>
</root>
<logger name="de.oaa.linkster" level="INFO" additivity="false">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE_ERRORS"/>
</logger>
</configuration>

View File

@@ -0,0 +1,256 @@
[
{
"id": "hitster-bingo-deutschland",
"name": "Hitster Bingo Deutschland",
"matcher": "/de/aaaa0019/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "full",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-ca",
"name": "Hitster Canada",
"matcher": "/ca/aaad0001/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-ca-franco",
"name": "Hitster Canada (Franco)",
"matcher": "/ca/aaad0002/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "no",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-ca-guilty-pleasures",
"name": "Hitster Guilty Pleasures (CA)",
"matcher": "/ca/aaad0003/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "full",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-de",
"name": "Hitster Deutsch",
"matcher": "/de/(\\d{5})$/",
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "full",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
},
"available": true
},
{
"id": "hitster-de-bayern-1-expansion",
"name": "Hitster Bayern 1 Expansion (DE)",
"matcher": "/de/aaaa0025/(\\d{5})$/",
"support": {
"appleMusic": "full",
"amazonMusic": "no",
"deezer": "full",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
},
"available": true
},
{
"id": "hitster-de-celebration",
"name": "Hitster Celebration (DE)",
"matcher": "/de/aaaa0040/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "no",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-de-guilty-pleasures",
"name": "Hitster Guilty Pleasures (DE)",
"matcher": "/de/aaaa0015/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-de-rock",
"name": "Hitster Rock (DE)",
"matcher": "/de/aaaa0039/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "full",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-de-schlager-party",
"name": "Hitster Schlager Party (DE)",
"matcher": "/de/aaaa0007/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "no",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-de-soundtracks-expansion",
"name": "Hitster Soundtracks Expansion (DE)",
"matcher": "/de/aaaa0026/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-de-summer-party",
"name": "Hitster Summer Party (DE)",
"matcher": "/de/aaaa0012/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-guilty-pleasures-nor",
"name": "Hitster Guilty Pleasures (NOR)",
"matcher": "/dk/aaaa0024/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "full",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-it",
"name": "Hitster Italia",
"matcher": "/it/aaac0001/(\\d{5})$/",
"available": true,
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "full",
"youtubeMusic": "full"
}
},
{
"id": "hitster-uk",
"name": "Hitster UK",
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "no",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "no",
"youtubeMusic": "no"
},
"available": false
},
{
"id": "hitster-usa",
"name": "Hitster USA",
"support": {
"appleMusic": "full",
"amazonMusic": "full",
"deezer": "full",
"soundcloud": "no",
"spotify": "full",
"tidal": "full",
"youtube": "no",
"youtubeMusic": "no"
},
"available": false
}
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More