From 5129cafa4ec785b8c6d2940f88f9f57c909e1d03 Mon Sep 17 00:00:00 2001 From: definitly486 Date: Tue, 27 Oct 2026 18:42:08 +0000 Subject: [PATCH 1/3] Subscribe to default channels on first launch and refresh missing avatars --- .../io/github/aedev/flow/FlowApplication.kt | 5 +++ .../aedev/flow/data/local/DefaultChannels.kt | 33 ++++++++++++++ .../flow/data/local/SubscriptionRepository.kt | 44 ++++++++++++++++++- 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/io/github/aedev/flow/data/local/DefaultChannels.kt diff --git a/app/src/main/java/io/github/aedev/flow/FlowApplication.kt b/app/src/main/java/io/github/aedev/flow/FlowApplication.kt index 8f38f8e4b1..00e29ce0f3 100644 --- a/app/src/main/java/io/github/aedev/flow/FlowApplication.kt +++ b/app/src/main/java/io/github/aedev/flow/FlowApplication.kt @@ -8,6 +8,7 @@ import coil3.ImageLoader import coil3.PlatformContext import coil3.SingletonImageLoader import dagger.hilt.android.HiltAndroidApp +import io.github.aedev.flow.data.local.DefaultChannels import io.github.aedev.flow.data.local.PlayerPreferences import io.github.aedev.flow.data.local.SubscriptionRepository import io.github.aedev.flow.data.repository.NewPipeDownloader @@ -229,6 +230,10 @@ class FlowApplication : try { val repository = SubscriptionRepository.getInstance(this@FlowApplication) val youtubeRepository = YouTubeRepository.getInstance(playerPreferences) + val seeded = repository.seedOnce("default_channels_v1", DefaultChannels.subscriptions) + if (seeded > 0) { + Log.i(TAG, "Subscribed to $seeded default channels") + } val repaired = repository.repairVideoThumbnailSubscriptions { channelId -> // Startup's own fetches hold the InnerTube connections for a while; nothing waits on this. diff --git a/app/src/main/java/io/github/aedev/flow/data/local/DefaultChannels.kt b/app/src/main/java/io/github/aedev/flow/data/local/DefaultChannels.kt new file mode 100644 index 0000000000..73a4f1c071 --- /dev/null +++ b/app/src/main/java/io/github/aedev/flow/data/local/DefaultChannels.kt @@ -0,0 +1,33 @@ +package io.github.aedev.flow.data.local + +/** Channels subscribed once on first launch of a build that ships this list. */ +internal object DefaultChannels { + private const val CHANNEL_URL_PREFIX = "https://www.youtube.com/channel/" + + private val entries = + listOf( + "https://www.youtube.com/channel/UCjCNdxBqo7HZLMo9j9zNkjg" to "-ТАЁЖНЫЙ-", + "https://www.youtube.com/channel/UCY03gpyR__MuJtBpoSyIGnw" to "Droider", + "https://www.youtube.com/channel/UC9QCSnXY8JR32jYNUVU8rgA" to + "Solo wild hikes in Siberia from ABVGAT. Survival.", + "https://www.youtube.com/channel/UCdubelOloxR3wzwJG9x8YqQ" to "TV Rain", + "https://www.youtube.com/channel/UCBpeYNfLtLMuxBYeQTeeuog" to "Всё из за Метало Копа", + "https://www.youtube.com/channel/UCuUH2_MlIpWpJnWZKeyVGrQ" to "ЗА ВЕРСТУ", + "https://www.youtube.com/channel/UCgpSieplNxXxLXYAzJLLpng" to "Майкл Наки", + "https://www.youtube.com/channel/UC5bhU-LtufD3tl2C9OyAzkw" to "НАЕДИНЕ С ТАЙГОЙ", + "https://www.youtube.com/channel/UCRGiaVdWE7nvjH9Ak8-oIPw" to "Реки Севера. Northern Rivers", + "https://www.youtube.com/channel/UCk94O116OXhPAOWZM7S2Leg" to "Ремонтяш", + "https://www.youtube.com/channel/UC9Omlbb17r_o7WwQsyvGV-A" to "Руслан Шемякин", + "https://www.youtube.com/channel/UCX-Yk13ftllhgSt6FrOrO8A" to "hackmyth", + ) + + /** Avatars start empty and are filled in by the startup avatar repair. */ + val subscriptions: List = + entries.map { (url, name) -> + ChannelSubscription( + channelId = url.removePrefix(CHANNEL_URL_PREFIX), + channelName = name, + channelThumbnail = "", + ) + } +} diff --git a/app/src/main/java/io/github/aedev/flow/data/local/SubscriptionRepository.kt b/app/src/main/java/io/github/aedev/flow/data/local/SubscriptionRepository.kt index 88e7a96485..7534e4dd8e 100644 --- a/app/src/main/java/io/github/aedev/flow/data/local/SubscriptionRepository.kt +++ b/app/src/main/java/io/github/aedev/flow/data/local/SubscriptionRepository.kt @@ -101,6 +101,45 @@ class SubscriptionRepository private constructor( } } + /** + * Subscribe to [channels] a single time per [seedKey]. Channels already subscribed, or that the + * user has unsubscribed from, are left alone, and once the flag is set the user's later + * unsubscribes are never undone. Returns how many were added. + */ + suspend fun seedOnce( + seedKey: String, + channels: Collection, + ): Int { + var added = 0 + context.subscriptionsDataStore.edit { preferences -> + val flag = stringPreferencesKey("seeded_$seedKey") + if (preferences[flag] != null) return@edit + + val currentOrder = + preferences[stringPreferencesKey(SUBSCRIPTIONS_ORDER_KEY)] + .orEmpty() + .split(",") + .filter { it.isNotEmpty() } + val knownIds = currentOrder.toMutableSet() + val newIds = mutableListOf() + + channels.forEach { channel -> + if (preferences.contains(channelKey(channel.channelId))) return@forEach + if (preferences.contains(unsubscribedKey(channel.channelId))) return@forEach + preferences[channelKey(channel.channelId)] = serializeChannel(channel) + if (knownIds.add(channel.channelId)) newIds += channel.channelId + } + + if (newIds.isNotEmpty()) { + preferences[stringPreferencesKey(SUBSCRIPTIONS_ORDER_KEY)] = + (newIds.asReversed() + currentOrder).joinToString(",") + } + preferences[flag] = System.currentTimeMillis().toString() + added = newIds.size + } + return added + } + /** * Unsubscribe from a channel */ @@ -231,7 +270,10 @@ class SubscriptionRepository private constructor( val subscriptions = getAllSubscriptions().first() val repairs = subscriptions - .filter { ThumbnailUrlResolver.isUnusableChannelAvatar(it.channelThumbnail) } + .filter { + it.channelThumbnail.isBlank() || + ThumbnailUrlResolver.isUnusableChannelAvatar(it.channelThumbnail) + } .mapNotNull { subscription -> val avatar = fetchChannelThumbnail(subscription.channelId).trim() if (avatar.isNotEmpty() && !ThumbnailUrlResolver.isUnusableChannelAvatar(avatar)) { From d21a73809c030f12bcb3bf0c102a74a1f91186eb Mon Sep 17 00:00:00 2001 From: definitly486 Date: Tue, 27 Oct 2026 22:40:11 +0300 Subject: [PATCH 2/3] ci: build debug APK on every push Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01AoQaxEBTnXqHJfoXXNHSiL --- .github/workflows/build-debug.yml | 48 +++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .github/workflows/build-debug.yml diff --git a/.github/workflows/build-debug.yml b/.github/workflows/build-debug.yml new file mode 100644 index 0000000000..289bef6f71 --- /dev/null +++ b/.github/workflows/build-debug.yml @@ -0,0 +1,48 @@ +name: Build debug APK + +on: + push: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + build: + name: Build debug + runs-on: ubuntu-24.04 + timeout-minutes: 45 + + steps: + - name: Checkout code + uses: actions/checkout@v7 + + - name: Set up JDK 21 + uses: actions/setup-java@v6 + with: + java-version: '21' + distribution: 'temurin' + + - name: Use CI Gradle properties + run: mkdir -p ~/.gradle && cp .github/ci-gradle.properties ~/.gradle/gradle.properties + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Make gradlew executable + run: chmod +x ./gradlew + + - name: Build debug APK + run: ./gradlew assembleFossDebug --stacktrace + + - name: Upload APK + uses: actions/upload-artifact@v4 + with: + name: app-foss-debug-${{ github.sha }} + path: app/build/outputs/apk/foss/debug/*.apk + if-no-files-found: error + retention-days: 14 From c29a0feadb408ca0befa51338ab1f2078bd87e9e Mon Sep 17 00:00:00 2001 From: definitly486 Date: Tue, 27 Oct 2026 22:53:02 +0300 Subject: [PATCH 3/3] ci: add manual release APK build workflow Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01AoQaxEBTnXqHJfoXXNHSiL --- .github/workflows/build-release.yml | 78 +++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 .github/workflows/build-release.yml diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml new file mode 100644 index 0000000000..9d40f870a8 --- /dev/null +++ b/.github/workflows/build-release.yml @@ -0,0 +1,78 @@ +name: Build release APK + +# Manual run: Actions -> "Build release APK" -> Run workflow. +# Signed when the RELEASE_KEYSTORE_BASE64 secret exists, otherwise the APK is +# unsigned (the same fallback app/build.gradle.kts uses without a keystore). +on: + workflow_dispatch: + inputs: + flavor: + description: 'Flavor to build' + type: choice + default: Foss + options: + - Foss + - Github + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + build: + name: Build release + runs-on: ubuntu-24.04 + timeout-minutes: 60 + + steps: + - name: Checkout code + uses: actions/checkout@v7 + + - name: Set up JDK 21 + uses: actions/setup-java@v6 + with: + java-version: '21' + distribution: 'temurin' + + - name: Use CI Gradle properties + run: mkdir -p ~/.gradle && cp .github/ci-gradle.properties ~/.gradle/gradle.properties + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v6 + + - name: Make gradlew executable + run: chmod +x ./gradlew + + - name: Decode keystore + id: keystore + env: + RELEASE_KEYSTORE_BASE64: ${{ secrets.RELEASE_KEYSTORE_BASE64 }} + run: | + if [ -n "$RELEASE_KEYSTORE_BASE64" ]; then + echo "$RELEASE_KEYSTORE_BASE64" | base64 --decode > release.keystore + echo "signed=true" >> "$GITHUB_OUTPUT" + echo "Release keystore decoded." + else + echo "signed=false" >> "$GITHUB_OUTPUT" + echo "::warning::RELEASE_KEYSTORE_BASE64 is not set. Building an UNSIGNED release APK." + fi + + - name: Build release APK + run: ./gradlew :app:assemble${{ inputs.flavor }}Release --stacktrace + env: + STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.KEY_ALIAS }} + KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} + LASTFM_API_KEY: ${{ secrets.LASTFM_API_KEY }} + LASTFM_API_SECRET: ${{ secrets.LASTFM_API_SECRET }} + + - name: Upload APK + uses: actions/upload-artifact@v7 + with: + name: app-${{ inputs.flavor }}-release-${{ steps.keystore.outputs.signed == 'true' && 'signed' || 'unsigned' }}-${{ github.sha }} + path: app/build/outputs/apk/**/release/*.apk + if-no-files-found: error + retention-days: 30