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

Jellyfin audiobook download #1208

Draft
wants to merge 45 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
e158a08
basic boilerplate for a jellyfin connection UI
Oct 25, 2024
33934f9
can use ClearableTextField for URl input
Oct 25, 2024
c8a4ae8
labels for connect UI + connect button (does nothing for now)
Oct 25, 2024
d7c98f4
add jellyfin-sdk-swift package
Oct 26, 2024
fe5299a
untested code for doing an initial connection
Oct 25, 2024
9334983
add view variant for initial server connection
Oct 25, 2024
22007dc
adjust username/password fields and the top right button
Oct 25, 2024
94a687c
perform login & prevent other actions while waiting for the API
Oct 25, 2024
aaed8af
prefer early outs
Oct 25, 2024
d6b1222
add stubs for jellyfin library screen
Oct 26, 2024
320c7aa
move to coordinator + flow approach
Oct 26, 2024
9f0742e
coordinator owns client after login
Oct 26, 2024
2eff3bd
go to library screen if already logged in
Oct 26, 2024
ed8042e
pass coordinator to view models
Oct 26, 2024
df46559
list audiobooks from library
Oct 26, 2024
bdd31d6
view model takes care of loading the items on selection + lazy loading
Oct 26, 2024
df268f3
make previews work again by adding mock view model that doesn't need …
Oct 26, 2024
d0c8453
slightly more thread safe
Oct 26, 2024
fbe1501
if there's only one user view, select it automatically
Oct 26, 2024
45648ac
sort book result list
Oct 27, 2024
7ec8bda
show folder hierarchy
Oct 27, 2024
7b614ba
show library (server) name
Oct 27, 2024
8e0d812
show audiobook images
Oct 27, 2024
27b0a30
replace confusingly named userview by library item
Oct 28, 2024
f2c5e8a
remove an !
Oct 28, 2024
0f78bd3
move API calls to view model, for consistency
Oct 28, 2024
1c88f7e
cancel fetch when view disappears
Oct 28, 2024
527d895
handle task cancellation
Oct 28, 2024
5842645
extract item view and image to separate structs
Oct 28, 2024
91a0bdb
use shared item view for userviews as well
Oct 28, 2024
a001f4e
fix images not working for userviews
Oct 28, 2024
2930034
show placeholder when there is no image
Oct 28, 2024
5050cd0
add blurhash placeholders
Oct 28, 2024
efa6150
extract common initializer code
Oct 28, 2024
0d0f074
load artwork with correct size
Oct 28, 2024
bbce51c
improve layout
Oct 28, 2024
57b49c0
remove blue button tint from navigation links
Oct 28, 2024
a7c962e
add folder indicator
Oct 28, 2024
95981d3
code formatting
Oct 28, 2024
75bc449
ensure that the badge's frame is always square
Oct 28, 2024
a6f9162
first simple version of jellyfin file download
Oct 29, 2024
219756d
I changed my name :)
Oct 29, 2024
2acc98b
extract code related to sungle file download (from URL) into separate…
Oct 29, 2024
e3efd59
perform jellyfin download using the SingleFileDownloadService
Oct 29, 2024
0c0dfd8
hide jellyfin UI when starting download
Oct 29, 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
101 changes: 101 additions & 0 deletions BookPlayer.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"originHash" : "a666f382d5a4884d3a9212a8e4b32eb8280bc2409cc22962bfa19e19731621ac",
"originHash" : "ac51a28f8927f1f439722175648f4ad5fe8f47b1036af26dc1d79fc70f1b86e0",
"pins" : [
{
"identity" : "devicekit",
Expand All @@ -19,6 +19,15 @@
"version" : "2.7.3"
}
},
{
"identity" : "get",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kean/Get",
"state" : {
"revision" : "31249885da1052872e0ac91a2943f62567c0d96d",
"version" : "2.2.1"
}
},
{
"identity" : "idzswiftcommoncrypto",
"kind" : "remoteSourceControl",
Expand All @@ -28,6 +37,15 @@
"version" : "0.16.1"
}
},
{
"identity" : "jellyfin-sdk-swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/jellyfin/jellyfin-sdk-swift.git",
"state" : {
"revision" : "a0e848a7aaec55991610818de6128b15cfcec725",
"version" : "0.4.0"
}
},
{
"identity" : "kingfisher",
"kind" : "remoteSourceControl",
Expand Down Expand Up @@ -91,6 +109,15 @@
"version" : "1.1.0"
}
},
{
"identity" : "urlqueryencoder",
"kind" : "remoteSourceControl",
"location" : "https://github.com/CreateAPI/URLQueryEncoder",
"state" : {
"revision" : "4ce950479707ea109f229d7230ec074a133b15d7",
"version" : "0.2.1"
}
},
{
"identity" : "ziparchive",
"kind" : "remoteSourceControl",
Expand Down
4 changes: 4 additions & 0 deletions BookPlayer/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,13 +135,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate, BPLogger {
playbackService: playbackService,
playerManager: playerManager
)
let singleFileDownloadService = SingleFileDownloadService(
networkClient: NetworkClient()
)
let coreServices = CoreServices(
dataManager: dataManager,
accountService: accountService,
syncService: syncService,
libraryService: libraryService,
playbackService: playbackService,
playerManager: playerManager,
singleFileDownloadService: singleFileDownloadService,
playerLoaderService: playerLoaderService,
watchService: watchService
)
Expand Down
13 changes: 13 additions & 0 deletions BookPlayer/Base.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@
"cancel_download_title" = "Cancel download";
"remove_downloaded_file_title" = "Remove from device";
"download_from_url_title" = "Download from URL";
"download_from_jellyfin_title" = "Download from Jellyfin";
"done_title" = "Done";
"select_title" = "Select";
"sort_button_title" = "Sort";
Expand Down Expand Up @@ -319,3 +320,15 @@ We're working hard on providing a seamless experience, if possible, please conta
"Rewind ${interval}" = "Rewind ${interval}";
"settings_lock_orientation_title" = "Orientation Locked";
"more_title" = "More";
"jellyfin_connection_title" = "Jellyfin";
"jellyfin_connect_button" = "Connect";
"jellyfin_login_button" = "Log in";
"jellyfin_section_server_url" = "Server URL";
"jellyfin_server_url_placeholder" = "http://jellyfin.example.com:8096";
"jellyfin_section_server_url_footer" = "Connect to your Jellyfin server";
"jellyfin_section_server" = "Server";
"jellyfin_server_name_label" = "Name";
"jellyfin_server_url_label" = "URL";
"jellyfin_section_login" = "Log in";
"jellyfin_username_placeholder" = "Username";
"jellyfin_password_placeholder" = "Password";
6 changes: 5 additions & 1 deletion BookPlayer/Coordinators/FolderListCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class FolderListCoordinator: ItemListCoordinator {
flow: BPCoordinatorPresentationFlow,
folderRelativePath: String,
playerManager: PlayerManagerProtocol,
singleFileDownloadService: SingleFileDownloadService,
libraryService: LibraryServiceProtocol,
playbackService: PlaybackServiceProtocol,
syncService: SyncServiceProtocol,
Expand All @@ -27,6 +28,7 @@ class FolderListCoordinator: ItemListCoordinator {
super.init(
flow: flow,
playerManager: playerManager,
singleFileDownloadService: singleFileDownloadService,
libraryService: libraryService,
playbackService: playbackService,
syncService: syncService,
Expand All @@ -40,7 +42,7 @@ class FolderListCoordinator: ItemListCoordinator {
let viewModel = ItemListViewModel(
folderRelativePath: self.folderRelativePath,
playerManager: self.playerManager,
networkClient: NetworkClient(),
singleFileDownloadService: self.singleFileDownloadService,
libraryService: self.libraryService,
playbackService: self.playbackService,
syncService: self.syncService,
Expand All @@ -56,6 +58,8 @@ class FolderListCoordinator: ItemListCoordinator {
self.loadPlayer(relativePath)
case .showDocumentPicker:
self.showDocumentPicker()
case .showJellyfinDownloader:
self.showJellyfinDownloader()
case .showSearchList(let relativePath, let placeholderTitle):
self.showSearchList(at: relativePath, placeholderTitle: placeholderTitle)
case .showItemDetails(let item):
Expand Down
15 changes: 15 additions & 0 deletions BookPlayer/Coordinators/ItemListCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import UniformTypeIdentifiers

class ItemListCoordinator: NSObject, Coordinator, AlertPresenter, BPLogger {
let playerManager: PlayerManagerProtocol
let singleFileDownloadService: SingleFileDownloadService
let libraryService: LibraryServiceProtocol
let playbackService: PlaybackServiceProtocol
let syncService: SyncServiceProtocol
Expand All @@ -22,9 +23,17 @@ class ItemListCoordinator: NSObject, Coordinator, AlertPresenter, BPLogger {

weak var documentPickerDelegate: UIDocumentPickerDelegate?

lazy var jellyfinCoordinator: JellyfinCoordinator = {
JellyfinCoordinator(
flow: .modalFlow(presentingController: flow.navigationController),
singleFileDownloadService: singleFileDownloadService
)
}()

init(
flow: BPCoordinatorPresentationFlow,
playerManager: PlayerManagerProtocol,
singleFileDownloadService: SingleFileDownloadService,
libraryService: LibraryServiceProtocol,
playbackService: PlaybackServiceProtocol,
syncService: SyncServiceProtocol,
Expand All @@ -33,6 +42,7 @@ class ItemListCoordinator: NSObject, Coordinator, AlertPresenter, BPLogger {
) {
self.flow = flow
self.playerManager = playerManager
self.singleFileDownloadService = singleFileDownloadService
self.libraryService = libraryService
self.playbackService = playbackService
self.syncService = syncService
Expand All @@ -54,6 +64,7 @@ class ItemListCoordinator: NSObject, Coordinator, AlertPresenter, BPLogger {
flow: .pushFlow(navigationController: flow.navigationController),
folderRelativePath: relativePath,
playerManager: playerManager,
singleFileDownloadService: singleFileDownloadService,
libraryService: libraryService,
playbackService: playbackService,
syncService: syncService,
Expand Down Expand Up @@ -160,6 +171,10 @@ extension ItemListCoordinator {
flow.navigationController.present(providerList, animated: true, completion: nil)
}

func showJellyfinDownloader() {
jellyfinCoordinator.start()
}

func showExportController(for items: [SimpleLibraryItem]) {
let providers = items.map { BookActivityItemProvider($0) }

Expand Down
15 changes: 5 additions & 10 deletions BookPlayer/Coordinators/LibraryListCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class LibraryListCoordinator: ItemListCoordinator, UINavigationControllerDelegat
init(
flow: BPCoordinatorPresentationFlow,
playerManager: PlayerManagerProtocol,
singleFileDownloadService: SingleFileDownloadService,
libraryService: LibraryServiceProtocol,
playbackService: PlaybackServiceProtocol,
syncService: SyncServiceProtocol,
Expand All @@ -40,6 +41,7 @@ class LibraryListCoordinator: ItemListCoordinator, UINavigationControllerDelegat
super.init(
flow: flow,
playerManager: playerManager,
singleFileDownloadService: singleFileDownloadService,
libraryService: libraryService,
playbackService: playbackService,
syncService: syncService,
Expand All @@ -54,7 +56,7 @@ class LibraryListCoordinator: ItemListCoordinator, UINavigationControllerDelegat
let viewModel = ItemListViewModel(
folderRelativePath: nil,
playerManager: self.playerManager,
networkClient: NetworkClient(),
singleFileDownloadService: self.singleFileDownloadService,
libraryService: self.libraryService,
playbackService: self.playbackService,
syncService: self.syncService,
Expand All @@ -70,6 +72,8 @@ class LibraryListCoordinator: ItemListCoordinator, UINavigationControllerDelegat
self.loadPlayer(relativePath)
case .showDocumentPicker:
self.showDocumentPicker()
case .showJellyfinDownloader:
self.showJellyfinDownloader()
case .showSearchList(let relativePath, let placeholderTitle):
self.showSearchList(at: relativePath, placeholderTitle: placeholderTitle)
case .showItemDetails(let item):
Expand Down Expand Up @@ -311,15 +315,6 @@ class LibraryListCoordinator: ItemListCoordinator, UINavigationControllerDelegat
lastItemListViewController.viewModel.viewDidAppear()
}

func handleDownloadAction(url: URL) {
guard
let libraryListViewController = flow.navigationController.viewControllers.first as? ItemListViewController
else { return }

libraryListViewController.setEditing(false, animated: false)
libraryListViewController.viewModel.handleDownload(url)
}

override func syncList() {
/// Process any deferred progress calculations for folders
if playbackService.processFoldersStaleProgress() {
Expand Down
3 changes: 3 additions & 0 deletions BookPlayer/Coordinators/MainCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class MainCoordinator: NSObject {
var tabBarController: AppTabBarController?

let playerManager: PlayerManagerProtocol
let singleFileDownloadService: SingleFileDownloadService
let libraryService: LibraryServiceProtocol
let playbackService: PlaybackServiceProtocol
let accountService: AccountServiceProtocol
Expand All @@ -36,6 +37,7 @@ class MainCoordinator: NSObject {
self.syncService = coreServices.syncService
self.playbackService = coreServices.playbackService
self.playerManager = coreServices.playerManager
self.singleFileDownloadService = coreServices.singleFileDownloadService
self.watchConnectivityService = coreServices.watchService

ThemeManager.shared.libraryService = libraryService
Expand Down Expand Up @@ -84,6 +86,7 @@ class MainCoordinator: NSObject {
let libraryCoordinator = LibraryListCoordinator(
flow: .pushFlow(navigationController: AppNavigationController.instantiate(from: .Main)),
playerManager: self.playerManager,
singleFileDownloadService: self.singleFileDownloadService,
libraryService: self.libraryService,
playbackService: self.playbackService,
syncService: syncService,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// JellyfinConnectionFormViewModel.swift
// BookPlayer
//
// Created by Lysann Tranvouez on 2024-10-25.
// Copyright © 2024 Tortuga Power. All rights reserved.
//

import Foundation

class JellyfinConnectionFormViewModel: ObservableObject {
@Published var serverUrl: String = ""
@Published var serverName: String?
@Published var username: String = ""
@Published var password: String = ""
}
106 changes: 106 additions & 0 deletions BookPlayer/Jellyfin/Connection Screen/JellyfinConnectionView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//
// JellyfinConnectionView.swift
// BookPlayer
//
// Created by Lysann Tranvouez on 2024-10-25.
// Copyright © 2024 Tortuga Power. All rights reserved.
//

import SwiftUI

struct JellyfinConnectionView: View {
/// View model for the form
@ObservedObject var viewModel: JellyfinConnectionViewModel
/// Theme view model to update colors
@StateObject var themeViewModel = ThemeViewModel()

struct DisconnectedView: View {
var serverUrl: Binding<String>
@EnvironmentObject var themeViewModel: ThemeViewModel

var body: some View {
Section {
ClearableTextField("jellyfin_server_url_placeholder".localized, text: serverUrl) {
$0.keyboardType = .URL
$0.textContentType = .URL
$0.autocapitalization = .none
}
} header: {
Text("jellyfin_section_server_url".localized)
.foregroundColor(themeViewModel.secondaryColor)
} footer: {
Text("jellyfin_section_server_url_footer".localized)
.foregroundColor(themeViewModel.secondaryColor)
}
}
}

struct FoundServerView: View {
var serverUrl: String
var serverName: String
var username: Binding<String>
var password: Binding<String>
@EnvironmentObject var themeViewModel: ThemeViewModel

var body: some View {
Section {
HStack {
Text("jellyfin_server_name_label".localized)
.foregroundColor(themeViewModel.secondaryColor)
Spacer()
Text(serverName)
}
HStack {
Text("jellyfin_server_url_label".localized)
.foregroundColor(themeViewModel.secondaryColor)
Spacer()
Text(serverUrl)
}
} header: {
Text("jellyfin_section_server".localized)
}
Section {
ClearableTextField("jellyfin_username_placeholder".localized, text: username) {
$0.textContentType = .name
$0.autocapitalization = .none
}
ClearableTextField("jellyfin_password_placeholder".localized, text: password) {
$0.textContentType = .password
$0.autocapitalization = .none
}
} header: {
Text("jellyfin_section_login".localized)
}
}
}

struct ConnectedView: View {
var body: some View {
EmptyView()
}
}

var body: some View {
Form {
switch viewModel.connectionState {
case .disconnected:
DisconnectedView(serverUrl: $viewModel.form.serverUrl)
case .foundServer:
FoundServerView(
serverUrl: viewModel.form.serverUrl,
serverName: viewModel.form.serverName ?? "",
username: $viewModel.form.username,
password: $viewModel.form.password
)
case .connected:
ConnectedView()
}
}
.environmentObject(themeViewModel)
}
}

#Preview {
var viewModel = JellyfinConnectionViewModel()
JellyfinConnectionView(viewModel: viewModel)
}
Loading