Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/build-debug.yml
Original file line number Diff line number Diff line change
@@ -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
78 changes: 78 additions & 0 deletions .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions app/src/main/java/io/github/aedev/flow/FlowApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ChannelSubscription> =
entries.map { (url, name) ->
ChannelSubscription(
channelId = url.removePrefix(CHANNEL_URL_PREFIX),
channelName = name,
channelThumbnail = "",
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<ChannelSubscription>,
): 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<String>()

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
*/
Expand Down Expand Up @@ -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)) {
Expand Down
Loading