// Download.swift import Foundation import ArgumentParser import JellyfinAPI extension Seanut { struct DownloadCommand: AsyncParsableCommand { static var configuration = CommandConfiguration( commandName: "download", abstract: "Downloads media into specified directory (default to current directory)" ) @OptionGroup var options: Seanut.CommonArguments @Argument(help: "media id returned from search") var mediaId: String @Option(name: .shortAndLong, help: "specifies that we're only looking for a specific season of a show") var season: Int? @Option(name: .shortAndLong, help: "location to save downloaded media") var output: String = FileManager.default.currentDirectoryPath static var userId: String! static var client: JellyfinClient! static let TypesWithChildren: [JellyfinAPI.BaseItemKind] = [ .musicAlbum, .musicGenre, .musicArtist, .playlist, .series, .boxSet ] func createDirectory(_ path: String) { do { try FileManager.default.createDirectory( atPath: path.expandingTildeInPath.removingPercentEncoding!, withIntermediateDirectories: true ) } catch { fatalError("unable to create output directory at \(path).") } } func downloadRoot(id: String, outputPath: URL) async { let downloadInfo = Paths.getItem(userID: DownloadCommand.userId, itemID: id) guard let rootInfo = try? await DownloadCommand.client.send(downloadInfo).value else { return } if DownloadCommand.TypesWithChildren.contains(rootInfo.type!) || rootInfo.childCount ?? 0 > 0 { let newPath = outputPath.appendingPathComponent(rootInfo.name!) createDirectory(String(newPath.absoluteString.suffix(newPath.absoluteString.count - 7))) let childrenParams = Paths.GetItemsParameters(userID: DownloadCommand.userId, parentID: id) guard let childInfo = try? await DownloadCommand.client.send(Paths.getItems(parameters: childrenParams)).value else { return } for child in childInfo.items! { await downloadRoot(id: child.id!, outputPath: newPath) } } else { let pathUrl = URL(string: rootInfo.path!)! let itemName: String = rootInfo.name! + "." + pathUrl.pathExtension let filePath = outputPath.appendingPathComponent(itemName.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)!) await downloadItem(id: id, outputPath: filePath) } } func downloadItem(id: String, outputPath: URL) async { do { let response = try await DownloadCommand.client.download(for: Paths.getDownload(itemID: id)) try FileManager.default.moveItem(at: response.location, to: outputPath) } catch { fatalError("Encountered \(error) downloading media. Please try again later.") } } mutating func run() async { // checks if our specified output directory exists, // if it doesn't then we create it before continuing if !FileManager.default.fileExists(atPath: output.expandingTildeInPath) { createDirectory(output) } DownloadCommand.client = JellyfinClient( configuration: Seanut.generateJellyfinConfiguration(url: options.domain.toURL()!), accessToken: Seanut.retrieveAccessToken(for: options.domain.toURL()!) ) do { DownloadCommand.userId = try await DownloadCommand.client.send(Paths.getCurrentUser).value.id! } catch { fatalError("failed to fetch user id. quitting...") } await downloadRoot(id: mediaId, outputPath: URL(fileURLWithPath: output)) } } }