// // Mastodon+API+Media.swift // // // Created by MainasuK Cirno on 2021-3-18. // import Foundation import Combine extension Mastodon.API.Media { static func uploadMediaEndpointURL(domain: String) -> URL { return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("media") } /// Upload media as attachment /// /// Creates an attachment to be used with a new status. /// /// - Since: 0.0.0 /// - Version: 3.3.0 /// # Last Update /// 2021/3/18 /// # Reference /// [Document](https://docs.joinmastodon.org/methods/statuses/media/) /// - Parameters: /// - session: `URLSession` /// - domain: Mastodon instance domain. e.g. "example.com" /// - query: `UploadMediaQuery` /// - authorization: User token /// - Returns: `AnyPublisher` contains `Attachment` nested in the response public static func uploadMedia( session: URLSession, domain: String, query: UploadMediaQuery, authorization: Mastodon.API.OAuth.Authorization? ) -> AnyPublisher, Error> { var request = Mastodon.API.post( url: uploadMediaEndpointURL(domain: domain), query: query, authorization: authorization ) request.timeoutInterval = 180 // should > 200 Kb/s for 40 MiB media attachment let serialStream = query.serialStream request.httpBodyStream = serialStream.boundStreams.input // total unit count in bytes count // will small than actally count due to multipart protocol meta serialStream.progress.totalUnitCount = { var size = 0 size += query.file?.sizeInByte ?? 0 size += query.thumbnail?.sizeInByte ?? 0 return Int64(size) }() query.progress.addChild( serialStream.progress, withPendingUnitCount: query.progress.totalUnitCount ) return session.dataTaskPublisher(for: request) .tryMap { data, response in let value = try Mastodon.API.decode(type: Mastodon.Entity.Attachment.self, from: data, response: response) return Mastodon.Response.Content(value: value, response: response) } .handleEvents(receiveCancel: { // retain and handle cancel task serialStream.boundStreams.output.close() }) .eraseToAnyPublisher() } public struct UploadMediaQuery: PostQuery, PutQuery { public let file: Mastodon.Query.MediaAttachment? public let thumbnail: Mastodon.Query.MediaAttachment? public let description: String? public let focus: String? public let progress: Progress = { let progress = Progress() progress.totalUnitCount = 100 return progress }() public init( file: Mastodon.Query.MediaAttachment?, thumbnail: Mastodon.Query.MediaAttachment?, description: String?, focus: String? ) { self.file = file self.thumbnail = thumbnail self.description = description self.focus = focus } var contentType: String? { return Self.multipartContentType() } var body: Data? { // using stream data return nil } var serialStream: SerialStream { var streams: [InputStream] = [] file.flatMap { value in streams.append(InputStream(data: Data.multipart(key: "file", value: value))) value.multipartStreamValue.flatMap { streams.append($0) } } thumbnail.flatMap { value in streams.append(InputStream(data: Data.multipart(key: "thumbnail", value: value))) value.multipartStreamValue.flatMap { streams.append($0) } } description.flatMap { value in streams.append(InputStream(data: Data.multipart(key: "description", value: value))) } focus.flatMap { value in streams.append(InputStream(data: Data.multipart(key: "focus", value: value))) } streams.append(InputStream(data: Data.multipartEnd())) return SerialStream(streams: streams) } public var clone: UploadMediaQuery { UploadMediaQuery(file: file, thumbnail: thumbnail, description: description, focus: focus) } } } extension Mastodon.API.Media { static func getMediaEndpointURL(domain: String, attachmentID: Mastodon.Entity.Attachment.ID) -> URL { Mastodon.API.endpointURL(domain: domain).appendingPathComponent("media").appendingPathComponent(attachmentID) } /// Get media attachment /// /// Get an Attachment, before it is attached to a status and posted, but after it is accepted for processing. /// /// - Since: 0.0.0 /// - Version: 3.4.1 /// # Last Update /// 2021/8/9 /// # Reference /// [Document](https://docs.joinmastodon.org/methods/statuses/media/) /// - Parameters: /// - session: `URLSession` /// - domain: Mastodon instance domain. e.g. "example.com" /// - mediaID: The ID of attachment /// - authorization: User token /// - Returns: `AnyPublisher` contains `Attachment` nested in the response public static func getMedia( session: URLSession, domain: String, attachmentID: Mastodon.Entity.Attachment.ID, authorization: Mastodon.API.OAuth.Authorization? ) -> AnyPublisher, Error> { var request = Mastodon.API.get( url: getMediaEndpointURL(domain: domain, attachmentID: attachmentID), query: nil, authorization: authorization ) request.timeoutInterval = 10 // short timeout for quick retry return session.dataTaskPublisher(for: request) .tryMap { data, response in let value = try Mastodon.API.decode(type: Mastodon.Entity.Attachment.self, from: data, response: response) return Mastodon.Response.Content(value: value, response: response) } .eraseToAnyPublisher() } } extension Mastodon.API.Media { static func updateMediaEndpointURL(domain: String, attachmentID: Mastodon.Entity.Attachment.ID) -> URL { return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("media").appendingPathComponent(attachmentID) } /// Update attachment /// /// Update an Attachment, before it is attached to a status and posted.. /// /// - Since: 0.0.0 /// - Version: 3.3.0 /// # Last Update /// 2021/3/18 /// # Reference /// [Document](https://docs.joinmastodon.org/methods/statuses/media/) /// - Parameters: /// - session: `URLSession` /// - domain: Mastodon instance domain. e.g. "example.com" /// - query: `UploadMediaQuery` /// - authorization: User token /// - Returns: `AnyPublisher` contains `Attachment` nested in the response public static func updateMedia( session: URLSession, domain: String, attachmentID: Mastodon.Entity.Attachment.ID, query: UpdateMediaQuery, authorization: Mastodon.API.OAuth.Authorization? ) -> AnyPublisher, Error> { var request = Mastodon.API.put( url: updateMediaEndpointURL(domain: domain, attachmentID: attachmentID), query: query, authorization: authorization ) request.timeoutInterval = 180 // should > 200 Kb/s for 40 MiB media attachment return session.dataTaskPublisher(for: request) .tryMap { data, response in let value = try Mastodon.API.decode(type: Mastodon.Entity.Attachment.self, from: data, response: response) return Mastodon.Response.Content(value: value, response: response) } .eraseToAnyPublisher() } public struct UpdateMediaQuery: PutQuery { public let file: Mastodon.Query.MediaAttachment? public let thumbnail: Mastodon.Query.MediaAttachment? public let description: String? public let focus: String? public init( file: Mastodon.Query.MediaAttachment?, thumbnail: Mastodon.Query.MediaAttachment?, description: String?, focus: String? ) { self.file = file self.thumbnail = thumbnail self.description = description self.focus = focus } var contentType: String? { return Self.multipartContentType() } var queryItems: [URLQueryItem]? { return nil } var body: Data? { var data = Data() // not modify uploaded binary data description.flatMap { data.append(Data.multipart(key: "description", value: $0)) } focus.flatMap { data.append(Data.multipart(key: "focus", value: $0)) } data.append(Data.multipartEnd()) return data } } }