Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add in app download functionality #1404

Open
wants to merge 36 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
53f5ec3
Add initial code for client internal downloads
May 27, 2024
bfd25c9
Add download to internal storage
May 27, 2024
7258688
Extend ExoPlayer to be able to play downloads
May 28, 2024
114c2de
Store Download Information separately
Jun 3, 2024
b133929
Working Downloads Fragment
Jun 3, 2024
6486fbe
Add openDownloads to NativeInterface
Jun 4, 2024
b2978eb
Add ability to remove downloads
Jun 4, 2024
56179f9
Improve Download Method Check
Jun 4, 2024
b25b6b2
Add detail to DownloadItem
Jun 4, 2024
109ec0d
Add option in settings to choose between external and internal download
Jun 4, 2024
a039e9f
Update layout dimensions
Jun 4, 2024
bff1b17
Fix order of deletion
Jun 4, 2024
ce17f38
Improve download speed
Jun 6, 2024
874b639
Improve exception handling for missing files
Jun 6, 2024
fa9937c
Improve layout and localization
Jun 6, 2024
d3b7ae5
Move media and subtitle download to ExoPlayer Download Service
Jun 24, 2024
ed904f2
Resolve detekt errors
neBM Jun 30, 2024
db53c86
Abstract JellyfinMediaSource
neBM Jun 30, 2024
3c85198
Rename KILOBYTE const to better communicate it's function
neBM Jul 2, 2024
5bdee10
Add support for selecting audio and encoded subtitle stream
neBM Jul 6, 2024
64ca273
Flatten DownloadItem & DownloadEntity into LocalJellyfinMediaSource
neBM Jul 6, 2024
8b9b8d9
Resolve detekt issues
neBM Jul 7, 2024
51d0ea2
Inline `prepareDownloadStreams` in QueueManager
neBM Jul 8, 2024
051de01
Merge pull request #1 from neBM/download
7ritn Jul 8, 2024
599851f
Merge branch 'master' into download
7ritn Jul 8, 2024
6b0817b
Fix build issue
Jul 8, 2024
b421059
Fix some more linting
Jul 8, 2024
57a112e
Switch to view binding
Jul 9, 2024
f0feb33
Remove obsolete files
Jul 9, 2024
068845f
Adjust layout
Jul 9, 2024
97dc624
Finally fix detekt
Jul 16, 2024
428bec3
Fix discard of some parallel downloads
Aug 18, 2024
7ce600c
Fix source error on HLS content
Aug 20, 2024
6f942e0
Move byte size conversion to separate file
Aug 22, 2024
6db98b7
Improve download notifications
Aug 22, 2024
48c9eb4
Fix detekt
Aug 22, 2024
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
4 changes: 4 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ plugins {
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.ksp)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.detekt)
alias(libs.plugins.android.junit5)
}
Expand Down Expand Up @@ -83,6 +84,7 @@ android {
buildConfig = true
viewBinding = true
compose = true
dataBinding = true
7ritn marked this conversation as resolved.
Show resolved Hide resolved
}
kotlinOptions {
@Suppress("SuspiciousCollectionReassignment")
Expand Down Expand Up @@ -113,6 +115,7 @@ dependencies {

// Kotlin
implementation(libs.bundles.coroutines)
implementation(libs.kotlin.serialization.json)

// Core
implementation(libs.bundles.koin)
Expand Down Expand Up @@ -146,6 +149,7 @@ dependencies {
}
}
implementation(libs.okhttp)
implementation(libs.okio)
implementation(libs.coil)
implementation(libs.cronet.embedded)

Expand Down
8 changes: 4 additions & 4 deletions app/schemas/org.jellyfin.mobile.data.JellyfinDatabase/2.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
],
"autoGenerate": true
]
},
"indices": [
{
Expand Down Expand Up @@ -82,10 +82,10 @@
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
],
"autoGenerate": true
]
},
"indices": [
{
Expand Down
183 changes: 183 additions & 0 deletions app/schemas/org.jellyfin.mobile.data.JellyfinDatabase/3.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "4366575e1609ecf3e2c44abd106d51d6",
"entities": [
{
"tableName": "Server",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `hostname` TEXT NOT NULL, `last_used_timestamp` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "hostname",
"columnName": "hostname",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastUsedTimestamp",
"columnName": "last_used_timestamp",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_Server_hostname",
"unique": true,
"columnNames": [
"hostname"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Server_hostname` ON `${TABLE_NAME}` (`hostname`)"
}
],
"foreignKeys": []
},
{
"tableName": "User",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `server_id` INTEGER NOT NULL, `user_id` TEXT NOT NULL, `access_token` TEXT, `last_login_timestamp` INTEGER NOT NULL, FOREIGN KEY(`server_id`) REFERENCES `Server`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "serverId",
"columnName": "server_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userId",
"columnName": "user_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accessToken",
"columnName": "access_token",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastLoginTimestamp",
"columnName": "last_login_timestamp",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_User_server_id_user_id",
"unique": true,
"columnNames": [
"server_id",
"user_id"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_User_server_id_user_id` ON `${TABLE_NAME}` (`server_id`, `user_id`)"
}
],
"foreignKeys": [
{
"table": "Server",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"server_id"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "Download",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `item_id` TEXT NOT NULL, `media_uri` TEXT NOT NULL, `media_source` TEXT NOT NULL, `download_folder_uri` TEXT NOT NULL, `download_length` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "itemId",
"columnName": "item_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "mediaUri",
"columnName": "media_uri",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "mediaSource",
"columnName": "media_source",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "downloadFolderUri",
"columnName": "download_folder_uri",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "downloadLength",
"columnName": "download_length",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_Download_item_id",
"unique": true,
"columnNames": [
"item_id"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Download_item_id` ON `${TABLE_NAME}` (`item_id`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4366575e1609ecf3e2c44abd106d51d6')"
]
}
}
15 changes: 15 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
android:required="false"
android:usesPermissionFlags="neverForLocation"
tools:targetApi="s" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />

<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
Expand All @@ -26,6 +28,8 @@
android:maxSdkVersion="28"
tools:ignore="ScopedStorage" />

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
Expand Down Expand Up @@ -79,6 +83,17 @@
</intent-filter>
</service>

<service android:name="org.jellyfin.mobile.downloads.JellyfinDownloadService"
android:exported="false"
android:foregroundServiceType="dataSync">
<!-- This is needed for Scheduler -->
<intent-filter>
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</service>


<receiver
android:name="androidx.mediarouter.media.MediaTransferReceiver"
android:exported="true"
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/assets/native/nativeshell.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ window.NativeShell = {
window.NativeInterface.openClientSettings();
},

openDownloads() {
window.NativeInterface.openDownloads();
},

selectServer() {
window.NativeInterface.openServerSelection();
},
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/org/jellyfin/mobile/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class MainViewModel(

private suspend fun refreshServer() {
val serverEntity = apiClientController.loadSavedServer()
_serverState.value = serverEntity?.let { entity -> ServerState.Available(entity) } ?: ServerState.Unset
_serverState.value = serverEntity?.let { entity -> ServerState.Available(entity) } ?: ServerState.Unset
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package org.jellyfin.mobile.app

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jellyfin.mobile.data.dao.DownloadDao
import org.jellyfin.mobile.data.dao.ServerDao
import org.jellyfin.mobile.data.dao.UserDao
import org.jellyfin.mobile.data.entity.DownloadEntity
import org.jellyfin.mobile.data.entity.ServerEntity
import org.jellyfin.sdk.Jellyfin
import org.jellyfin.sdk.api.client.ApiClient
Expand All @@ -15,7 +17,7 @@ class ApiClientController(
private val jellyfin: Jellyfin,
private val apiClient: ApiClient,
private val serverDao: ServerDao,
private val userDao: UserDao,
private val userDao: UserDao
) {
private val baseDeviceInfo: DeviceInfo
get() = jellyfin.options.deviceInfo!!
Expand Down
Loading
Loading