YouTube-Downloader ueber den Baka-Proxy inkl. Anmeldung und MediaStore
Neuer YouTube-Bereich im Online-Tab: Adresse einfuegen, herunterladen, der Titel landet in Meine Musik. - BakaAuth: Anmeldung gegen baka-net.de/auth, Token verschluesselt auf dem Geraet. Der Server meldet Fehler mit HTTP 200 und Text im Rumpf, deshalb wird der Inhalt geprueft statt nur der Statuscode. - YtDownloadService: Proxy fragen, MP3 abholen, halbe Dateien aufraeumen. Kuenstler nur aus "Kuenstler - Lied", sonst leer statt geraten. - MediaStoreBridge (Kotlin): legt die Datei in Music/Melo ab und meldet sie dem MediaStore. Noetig, weil der Android-Scan sonst beim naechsten Lauf alles wieder als verschwunden markiert. - SubTabs nach shared/ gezogen (jetzt in Meine Musik und Online genutzt). 177 Tests gruen, flutter analyze ohne Befund, Release-APK baut. Der Weg ueber den echten Proxy ist noch nicht auf dem Geraet erprobt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018NLW1q1hzEC9goDQ1dJ98d
This commit is contained in:
co-authored by
Claude Opus 5
parent
a625d70da8
commit
6200c77614
@@ -13,6 +13,10 @@
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32"/>
|
||||
<!-- Heruntergeladene Musik in Music/Melo ablegen. Ab Android 10 uebernimmt
|
||||
das der MediaStore ohne Berechtigung, davor braucht es Schreibrechte. -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28"/>
|
||||
<application
|
||||
android:label="melo"
|
||||
android:name="${applicationName}"
|
||||
|
||||
@@ -15,6 +15,12 @@ class MainActivity : AudioServiceActivity() {
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
AudioEffectsBridge.CHANNEL
|
||||
).setMethodCallHandler(bridge::handle)
|
||||
|
||||
val mediaStore = MediaStoreBridge(applicationContext)
|
||||
MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
MediaStoreBridge.CHANNEL
|
||||
).setMethodCallHandler(mediaStore::handle)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
package de.baka.melo
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Traegt heruntergeladene Musik in den oeffentlichen Musikordner ein
|
||||
* (Music/Melo) und meldet sie dem MediaStore.
|
||||
*
|
||||
* Warum ueberhaupt: der Bibliotheks-Scan liest auf Android den MediaStore.
|
||||
* Eine Datei, die dort nicht steht, waere beim naechsten Scan wieder weg.
|
||||
*/
|
||||
class MediaStoreBridge(private val context: Context) {
|
||||
|
||||
companion object {
|
||||
const val CHANNEL = "de.baka.melo/media_store"
|
||||
private const val UNTERORDNER = "Melo"
|
||||
}
|
||||
|
||||
fun handle(call: MethodCall, result: MethodChannel.Result) {
|
||||
if (call.method != "publishAudio") {
|
||||
result.notImplemented()
|
||||
return
|
||||
}
|
||||
val quelle = call.argument<String>("sourcePath")
|
||||
if (quelle == null) {
|
||||
result.error("args", "sourcePath fehlt", null)
|
||||
return
|
||||
}
|
||||
try {
|
||||
result.success(
|
||||
publish(
|
||||
File(quelle),
|
||||
call.argument<String>("title") ?: File(quelle).nameWithoutExtension,
|
||||
call.argument<String>("artist")
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
result.error("io", e.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun publish(quelle: File, titel: String, kuenstler: String?): String {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
publishViaMediaStore(quelle, titel, kuenstler)
|
||||
} else {
|
||||
publishViaDateisystem(quelle, titel, kuenstler)
|
||||
}
|
||||
}
|
||||
|
||||
/** Android 10+: Scoped Storage — der MediaStore vergibt den Platz. */
|
||||
private fun publishViaMediaStore(quelle: File, titel: String, kuenstler: String?): String {
|
||||
val werte = ContentValues().apply {
|
||||
put(MediaStore.Audio.Media.DISPLAY_NAME, quelle.name)
|
||||
put(MediaStore.Audio.Media.MIME_TYPE, "audio/mpeg")
|
||||
put(MediaStore.Audio.Media.TITLE, titel)
|
||||
put(MediaStore.Audio.Media.IS_MUSIC, 1)
|
||||
kuenstler?.takeIf { it.isNotBlank() }
|
||||
?.let { put(MediaStore.Audio.Media.ARTIST, it) }
|
||||
put(
|
||||
MediaStore.Audio.Media.RELATIVE_PATH,
|
||||
"${Environment.DIRECTORY_MUSIC}/$UNTERORDNER"
|
||||
)
|
||||
// Solange 1, sieht keine andere App die halbfertige Datei.
|
||||
put(MediaStore.Audio.Media.IS_PENDING, 1)
|
||||
}
|
||||
|
||||
val aufloeser = context.contentResolver
|
||||
val ziel = aufloeser.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, werte)
|
||||
?: throw IllegalStateException("MediaStore nahm den Eintrag nicht an")
|
||||
|
||||
try {
|
||||
aufloeser.openOutputStream(ziel).use { aus ->
|
||||
requireNotNull(aus) { "Kein Schreibzugriff auf den Eintrag" }
|
||||
quelle.inputStream().use { ein -> ein.copyTo(aus) }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
aufloeser.delete(ziel, null, null)
|
||||
throw e
|
||||
}
|
||||
|
||||
aufloeser.update(
|
||||
ziel,
|
||||
ContentValues().apply { put(MediaStore.Audio.Media.IS_PENDING, 0) },
|
||||
null,
|
||||
null
|
||||
)
|
||||
quelle.delete()
|
||||
|
||||
// Der Scan gleicht ueber den Dateipfad ab, nicht ueber die URI —
|
||||
// deshalb den echten Pfad nachschlagen.
|
||||
aufloeser.query(ziel, arrayOf(MediaStore.Audio.Media.DATA), null, null, null)
|
||||
?.use { c ->
|
||||
if (c.moveToFirst()) {
|
||||
c.getString(0)?.let { return it }
|
||||
}
|
||||
}
|
||||
return ziel.toString()
|
||||
}
|
||||
|
||||
/** Android 9 und aelter: direkt in den Musikordner, dann scannen lassen. */
|
||||
private fun publishViaDateisystem(quelle: File, titel: String, kuenstler: String?): String {
|
||||
val ordner = File(
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC),
|
||||
UNTERORDNER
|
||||
)
|
||||
ordner.mkdirs()
|
||||
val ziel = File(ordner, quelle.name)
|
||||
quelle.copyTo(ziel, overwrite = true)
|
||||
quelle.delete()
|
||||
|
||||
val werte = ContentValues().apply {
|
||||
put(MediaStore.Audio.Media.DATA, ziel.absolutePath)
|
||||
put(MediaStore.Audio.Media.DISPLAY_NAME, ziel.name)
|
||||
put(MediaStore.Audio.Media.MIME_TYPE, "audio/mpeg")
|
||||
put(MediaStore.Audio.Media.TITLE, titel)
|
||||
put(MediaStore.Audio.Media.IS_MUSIC, 1)
|
||||
kuenstler?.takeIf { it.isNotBlank() }
|
||||
?.let { put(MediaStore.Audio.Media.ARTIST, it) }
|
||||
}
|
||||
context.contentResolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, werte)
|
||||
return ziel.absolutePath
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user