Skip to content
Merged
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
21 changes: 9 additions & 12 deletions BookPlayer/AudiobookShelf/AudiobookShelfRootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,24 +39,21 @@ struct AudiobookShelfRootView: View {
.navigationBarTitleDisplayMode(.inline)
.navigationDestination(for: AudiobookShelfLibraryLevelData.self) { destination in
switch destination {
case .topLevel(let libraryName):
AudiobookShelfLibraryView(
viewModel: AudiobookShelfLibraryViewModel(
libraryID: nil,
connectionService: connectionViewModel.connectionService,
singleFileDownloadService: singleFileDownloadService,
navigation: navigation,
navigationTitle: libraryName
)
case .library(source: .browseCategories(let library), title: _):
AudiobookShelfBrowseTabsView(
library: library,
connectionService: connectionViewModel.connectionService,
singleFileDownloadService: singleFileDownloadService,
navigation: navigation
)
case .library(let item):
case .library(source: let source, title: let title):
AudiobookShelfLibraryView(
viewModel: AudiobookShelfLibraryViewModel(
libraryID: item.id,
source: source,
connectionService: connectionViewModel.connectionService,
singleFileDownloadService: singleFileDownloadService,
navigation: navigation,
navigationTitle: item.title
navigationTitle: title
)
)
case .details(let item):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ final class AudiobookShelfConnectionViewModel: ObservableObject, BPLogger {

Task { @MainActor in
navigation.path.append(
AudiobookShelfLibraryLevelData.topLevel(libraryName: form.serverName)
AudiobookShelfLibraryLevelData.library(
source: AudiobookShelfLibraryViewSource.libraries,
title: form.serverName
)
)
}
} else {
Expand Down Expand Up @@ -78,7 +81,10 @@ final class AudiobookShelfConnectionViewModel: ObservableObject, BPLogger {

connectionState = .connected
navigation.path.append(
AudiobookShelfLibraryLevelData.topLevel(libraryName: form.serverName)
AudiobookShelfLibraryLevelData.library(
source: AudiobookShelfLibraryViewSource.libraries,
title: form.serverName
)
)
} catch let error as AudiobookShelfError {
throw error.localizedDescription
Expand All @@ -90,14 +96,18 @@ final class AudiobookShelfConnectionViewModel: ObservableObject, BPLogger {
@MainActor
func handleSignOutAction() {
connectionService.deleteConnection()
navigation.path = NavigationPath()
form = AudiobookShelfConnectionFormViewModel()
connectionState = .disconnected
}

@MainActor
func handleGoToLibraryAction() {
navigation.path.append(
AudiobookShelfLibraryLevelData.topLevel(libraryName: form.serverName)
AudiobookShelfLibraryLevelData.library(
source: AudiobookShelfLibraryViewSource.libraries,
title: form.serverName
)
)
}
}
225 changes: 215 additions & 10 deletions BookPlayer/AudiobookShelf/Library Screen/AudiobookShelfLibraryItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,52 @@

import Foundation

struct AudiobookShelfSeriesReference: Codable, Hashable {
let id: String
let name: String
let sequence: String?
}

struct AudiobookShelfLibraryItem: Identifiable, Hashable, Codable {
enum Kind: String, Codable {
case audiobook = "book"
case podcast = "podcast"
case library = "library"
case browseCategory = "browseCategory"
case series = "series"
case collection = "collection"
case author = "author"
case narrator = "narrator"
}

let id: String
let title: String
let kind: Kind
let libraryId: String

// Metadata
let authorName: String?
let narratorName: String?
let duration: TimeInterval?
let size: Int64?

let subtitle: String?
let series: [AudiobookShelfSeriesReference]?
let addedAt: Int64?
let updatedAt: Int64?

// Cover image
let coverPath: String?

let coverItemId: String?

// Progress (if included)
let progress: Double?
let currentTime: TimeInterval?
let isFinished: Bool?


// Browse metadata
let browseCategory: AudiobookShelfBrowseCategory?
let filter: AudiobookShelfItemFilter?

init(
id: String,
title: String,
Expand All @@ -43,10 +63,17 @@ struct AudiobookShelfLibraryItem: Identifiable, Hashable, Codable {
narratorName: String? = nil,
duration: TimeInterval? = nil,
size: Int64? = nil,
subtitle: String? = nil,
series: [AudiobookShelfSeriesReference]? = nil,
addedAt: Int64? = nil,
updatedAt: Int64? = nil,
coverPath: String? = nil,
coverItemId: String? = nil,
progress: Double? = nil,
currentTime: TimeInterval? = nil,
isFinished: Bool? = nil
isFinished: Bool? = nil,
browseCategory: AudiobookShelfBrowseCategory? = nil,
filter: AudiobookShelfItemFilter? = nil
) {
self.id = id
self.title = title
Expand All @@ -56,14 +83,118 @@ struct AudiobookShelfLibraryItem: Identifiable, Hashable, Codable {
self.narratorName = narratorName
self.duration = duration
self.size = size
self.subtitle = subtitle
self.series = series
self.addedAt = addedAt
self.updatedAt = updatedAt
self.coverPath = coverPath
self.coverItemId = coverItemId
self.progress = progress
self.currentTime = currentTime
self.isFinished = isFinished
self.browseCategory = browseCategory
self.filter = filter
}
}

extension AudiobookShelfLibraryItem {
var isDownloadable: Bool {
kind == .audiobook || kind == .podcast
}

var isNavigable: Bool {
!isDownloadable
}

var placeholderImageName: String {
switch kind {
case .podcast, .audiobook: "waveform"
case .library: "folder"
case .browseCategory:
switch browseCategory {
case .books: "books.vertical"
case .series: "rectangle.stack"
case .collections: "square.stack.3d.up"
case .authors: "person.2"
case .narrators: "mic"
case .none: "square.grid.2x2"
}
case .series: "rectangle.stack"
case .collection: "square.stack.3d.up"
case .author: "person"
case .narrator: "mic"
}
}

func seriesSequence(for seriesID: String) -> String? {
series?.first(where: { $0.id == seriesID })?.sequence
}

init(library: AudiobookShelfLibrary) {
self.init(
id: library.id,
title: library.name,
kind: .library,
libraryId: library.id,
subtitle: library.mediaType == "podcast" ? "Podcast library" : "Audiobook library"
)
}

init(category: AudiobookShelfBrowseCategory, libraryId: String) {
self.init(
id: category.rawValue,
title: category.title,
kind: .browseCategory,
libraryId: libraryId,
subtitle: "Browse by \(category.title.lowercased())",
browseCategory: category
)
}

init(author: AudiobookShelfLibraryFilterData.NamedEntity, libraryId: String) {
self.init(
id: author.id,
title: author.name,
kind: .author,
libraryId: libraryId,
subtitle: "Author",
filter: AudiobookShelfItemFilter(group: .authors, value: author.id, title: author.name)
)
}

init(series: AudiobookShelfLibraryFilterData.NamedEntity, libraryId: String) {
self.init(
id: series.id,
title: series.name,
kind: .series,
libraryId: libraryId,
subtitle: "Series",
filter: AudiobookShelfItemFilter(group: .series, value: series.id, title: series.name)
)
}

init(narrator: String, libraryId: String) {
self.init(
id: narrator,
title: narrator,
kind: .narrator,
libraryId: libraryId,
subtitle: "Narrator",
filter: AudiobookShelfItemFilter(group: .narrators, value: narrator, title: narrator)
)
}

init(collection: AudiobookShelfCollection) {
self.init(
id: collection.id,
title: collection.name,
kind: .collection,
libraryId: collection.libraryId,
subtitle: collection.description ?? "\(collection.books.count) books",
coverItemId: collection.books.first?.id
)
}

init?(apiItem: AudiobookShelfAPIItem) {
guard let mediaType = apiItem.mediaType,
let kind = Kind(rawValue: mediaType) else {
Expand All @@ -75,10 +206,13 @@ extension AudiobookShelfLibraryItem {
title: apiItem.media.metadata.title,
kind: kind,
libraryId: apiItem.libraryId,
authorName: apiItem.media.metadata.authorName,
narratorName: apiItem.media.metadata.narratorName,
authorName: apiItem.media.metadata.primaryAuthorName,
narratorName: apiItem.media.metadata.primaryNarratorName,
duration: apiItem.media.duration,
size: apiItem.size,
series: apiItem.media.metadata.series,
addedAt: apiItem.addedAt,
updatedAt: apiItem.updatedAt,
coverPath: apiItem.media.coverPath,
progress: apiItem.userMediaProgress?.progress,
currentTime: apiItem.userMediaProgress?.currentTime,
Expand All @@ -92,23 +226,68 @@ extension AudiobookShelfLibraryItem {
struct AudiobookShelfAPIItem: Codable {
let id: String
let libraryId: String
let addedAt: Int64?
let updatedAt: Int64?
let mediaType: String?
let media: Media
let size: Int64?
let userMediaProgress: UserMediaProgress?

struct Media: Codable {
let metadata: Metadata
let coverPath: String?
let duration: TimeInterval?

struct Metadata: Codable {
let title: String
let authorName: String?
let narratorName: String?
let authors: [NamedEntity]?
let narrators: [String]?
let series: [AudiobookShelfSeriesReference]?

enum CodingKeys: String, CodingKey {
case title
case authorName
case narratorName
case authors
case narrators
case series
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

title = try container.decode(String.self, forKey: .title)
authorName = try container.decodeIfPresent(String.self, forKey: .authorName)
narratorName = try container.decodeIfPresent(String.self, forKey: .narratorName)
authors = try container.decodeIfPresent([NamedEntity].self, forKey: .authors)
narrators = try container.decodeIfPresent([String].self, forKey: .narrators)

if let seriesArray = try? container.decode([AudiobookShelfSeriesReference].self, forKey: .series) {
series = seriesArray
} else if let seriesSingle = try? container.decode(AudiobookShelfSeriesReference.self, forKey: .series) {
series = [seriesSingle]
} else {
series = nil
}
}

var primaryAuthorName: String? {
authorName ?? authors?.first?.name
}

var primaryNarratorName: String? {
narratorName ?? narrators?.first
}
}

struct NamedEntity: Codable {
let id: String
let name: String
}
}

struct UserMediaProgress: Codable {
let progress: Double
let currentTime: TimeInterval
Expand All @@ -130,3 +309,29 @@ struct AudiobookShelfSearchResponse: Codable {
let libraryItem: AudiobookShelfAPIItem
}
}

struct AudiobookShelfLibraryFilterData: Codable {
let authors: [NamedEntity]
let genres: [String]
let tags: [String]
let series: [NamedEntity]
let narrators: [String]
let languages: [String]

struct NamedEntity: Codable, Hashable {
let id: String
let name: String
}
}

struct AudiobookShelfCollection: Codable {
let id: String
let libraryId: String
let name: String
let description: String?
let books: [AudiobookShelfAPIItem]
}

struct AudiobookShelfCollectionsResponse: Codable {
let results: [AudiobookShelfCollection]
}
Loading