2024-08-29 11:44:05 +04:00

421 lines
17 KiB
Swift

import Foundation
import SwiftSignalKit
import Postbox
import TelegramApi
public enum MediaResourceUserContentType: UInt8, Equatable {
case other = 0
case image = 1
case video = 2
case audio = 3
case file = 4
case sticker = 6
case avatar = 7
case audioVideoMessage = 8
case story = 9
}
public extension MediaResourceUserContentType {
init(file: TelegramMediaFile) {
if file.isInstantVideo || file.isVoice {
self = .audioVideoMessage
} else if file.isMusic {
self = .audio
} else if file.isSticker || file.isAnimatedSticker {
self = .sticker
} else if file.isCustomEmoji {
self = .sticker
} else if file.isVideo {
if file.isAnimated {
self = .other
} else {
self = .video
}
} else {
self = .file
}
}
}
public extension MediaResourceFetchParameters {
init(tag: MediaResourceFetchTag?, info: MediaResourceFetchInfo?, location: MediaResourceStorageLocation?, contentType: MediaResourceUserContentType, isRandomAccessAllowed: Bool) {
self.init(tag: tag, info: info, location: location, contentType: contentType.rawValue, isRandomAccessAllowed: isRandomAccessAllowed)
}
}
func bufferedFetch(_ signal: Signal<EngineMediaResource.Fetch.Result, EngineMediaResource.Fetch.Error>) -> Signal<EngineMediaResource.Fetch.Result, EngineMediaResource.Fetch.Error> {
return Signal { subscriber in
final class State {
var data = Data()
var isCompleted: Bool = false
init() {
}
}
let state = Atomic<State>(value: State())
return signal.start(next: { value in
switch value {
case let .dataPart(_, data, _, _):
let _ = state.with { state in
state.data.append(data)
}
case let .moveTempFile(file):
let _ = state.with { state in
state.isCompleted = true
}
subscriber.putNext(.moveTempFile(file: file))
case let .resourceSizeUpdated(size):
if size == 0 {
let _ = state.with { state in
state.data.removeAll()
state.isCompleted = true
}
let tempFile = TempBox.shared.tempFile(fileName: "file")
let _ = try? Data().write(to: URL(fileURLWithPath: tempFile.path), options: .atomic)
subscriber.putNext(.moveTempFile(file: tempFile))
subscriber.putCompletion()
}
default:
assert(false)
break
}
}, error: { error in
subscriber.putError(error)
}, completed: {
let tempFile = state.with { state -> TempBoxFile? in
if state.isCompleted {
return nil
} else {
let tempFile = TempBox.shared.tempFile(fileName: "data")
let _ = try? state.data.write(to: URL(fileURLWithPath: tempFile.path), options: .atomic)
return tempFile
}
}
if let tempFile = tempFile {
subscriber.putNext(.moveTempFile(file: tempFile))
}
subscriber.putCompletion()
})
}
}
public final class EngineMediaResource: Equatable {
public enum CacheTimeout {
case `default`
case shortLived
}
public struct ByteRange {
public enum Priority {
case `default`
case elevated
case maximum
}
public var range: Range<Int>
public var priority: Priority
public init(range: Range<Int>, priority: Priority) {
self.range = range
self.priority = priority
}
}
public final class Fetch {
public enum Result {
case dataPart(resourceOffset: Int64, data: Data, range: Range<Int64>, complete: Bool)
case resourceSizeUpdated(Int64)
case progressUpdated(Float)
case replaceHeader(data: Data, range: Range<Int64>)
case moveLocalFile(path: String)
case moveTempFile(file: TempBoxFile)
case copyLocalItem(MediaResourceDataFetchCopyLocalItem)
case reset
}
public enum Error {
case generic
}
public let signal: () -> Signal<Result, Error>
public init(_ signal: @escaping () -> Signal<Result, Error>) {
self.signal = signal
}
}
public final class ResourceData {
public let path: String
public let availableSize: Int64
public let isComplete: Bool
public init(
path: String,
availableSize: Int64,
isComplete: Bool
) {
self.path = path
self.availableSize = availableSize
self.isComplete = isComplete
}
}
public enum FetchStatus: Equatable {
case Remote(progress: Float)
case Local
case Fetching(isActive: Bool, progress: Float)
case Paused(progress: Float)
}
public struct Id: Equatable, Hashable {
public var stringRepresentation: String
public init(_ stringRepresentation: String) {
self.stringRepresentation = stringRepresentation
}
public init(_ id: MediaResourceId) {
self.stringRepresentation = id.stringRepresentation
}
}
private let resource: MediaResource
public init(_ resource: MediaResource) {
self.resource = resource
}
public func _asResource() -> MediaResource {
return self.resource
}
public var id: Id {
return Id(self.resource.id)
}
public static func ==(lhs: EngineMediaResource, rhs: EngineMediaResource) -> Bool {
return lhs.resource.isEqual(to: rhs.resource)
}
}
public extension EngineMediaResource.ResourceData {
convenience init(_ data: MediaResourceData) {
self.init(path: data.path, availableSize: data.size, isComplete: data.complete)
}
}
public extension EngineMediaResource.FetchStatus {
init(_ status: MediaResourceStatus) {
switch status {
case let .Remote(progress):
self = .Remote(progress: progress)
case .Local:
self = .Local
case let .Fetching(isActive, progress):
self = .Fetching(isActive: isActive, progress: progress)
case let .Paused(progress):
self = .Paused(progress: progress)
}
}
func _asStatus() -> MediaResourceStatus {
switch self {
case let .Remote(progress):
return .Remote(progress: progress)
case .Local:
return .Local
case let .Fetching(isActive, progress):
return .Fetching(isActive: isActive, progress: progress)
case let .Paused(progress):
return .Paused(progress: progress)
}
}
}
public extension TelegramEngine {
final class Resources {
private let account: Account
init(account: Account) {
self.account = account
}
public func preUpload(id: Int64, encrypt: Bool, tag: MediaResourceFetchTag?, source: Signal<EngineMediaResource.ResourceData, NoError>, onComplete: (()->Void)? = nil) {
return self.account.messageMediaPreuploadManager.add(network: self.account.network, postbox: self.account.postbox, id: id, encrypt: encrypt, tag: tag, source: source, onComplete: onComplete)
}
public func collectCacheUsageStats(peerId: PeerId? = nil, additionalCachePaths: [String] = [], logFilesPath: String? = nil) -> Signal<CacheUsageStatsResult, NoError> {
return _internal_collectCacheUsageStats(account: self.account, peerId: peerId, additionalCachePaths: additionalCachePaths, logFilesPath: logFilesPath)
}
public func collectStorageUsageStats() -> Signal<AllStorageUsageStats, NoError> {
return _internal_collectStorageUsageStats(account: self.account)
}
public func renderStorageUsageStatsMessages(stats: StorageUsageStats, categories: [StorageUsageStats.CategoryKey], existingMessages: [EngineMessage.Id: Message]) -> Signal<[EngineMessage.Id: Message], NoError> {
return _internal_renderStorageUsageStatsMessages(account: self.account, stats: stats, categories: categories, existingMessages: existingMessages)
}
public func clearStorage(peerId: EnginePeer.Id?, categories: [StorageUsageStats.CategoryKey], includeMessages: [Message], excludeMessages: [Message]) -> Signal<Float, NoError> {
return _internal_clearStorage(account: self.account, peerId: peerId, categories: categories, includeMessages: includeMessages, excludeMessages: excludeMessages)
}
public func clearStorage(peerIds: Set<EnginePeer.Id>, includeMessages: [Message], excludeMessages: [Message]) -> Signal<Float, NoError> {
_internal_clearStorage(account: self.account, peerIds: peerIds, includeMessages: includeMessages, excludeMessages: excludeMessages)
}
public func clearStorage(messages: [Message]) -> Signal<Never, NoError> {
_internal_clearStorage(account: self.account, messages: messages)
}
public func clearCachedMediaResources(mediaResourceIds: Set<MediaResourceId>) -> Signal<Float, NoError> {
return _internal_clearCachedMediaResources(account: self.account, mediaResourceIds: mediaResourceIds)
}
public func reindexCacheInBackground(lowImpact: Bool) -> Signal<Never, NoError> {
let mediaBox = self.account.postbox.mediaBox
return _internal_reindexCacheInBackground(account: self.account, lowImpact: lowImpact)
|> then(Signal { subscriber in
return mediaBox.updateResourceIndex(otherResourceContentType: MediaResourceUserContentType.other.rawValue, lowImpact: lowImpact, completion: {
subscriber.putCompletion()
})
})
}
public func data(id: EngineMediaResource.Id, attemptSynchronously: Bool = false) -> Signal<EngineMediaResource.ResourceData, NoError> {
return self.account.postbox.mediaBox.resourceData(
id: MediaResourceId(id.stringRepresentation),
pathExtension: nil,
option: .complete(waitUntilFetchStatus: false),
attemptSynchronously: attemptSynchronously
)
|> map { data in
return EngineMediaResource.ResourceData(data)
}
}
public func custom(
id: String,
fetch: EngineMediaResource.Fetch?,
cacheTimeout: EngineMediaResource.CacheTimeout = .default,
attemptSynchronously: Bool = false
) -> Signal<EngineMediaResource.ResourceData, NoError> {
let mappedKeepDuration: CachedMediaRepresentationKeepDuration
switch cacheTimeout {
case .default:
mappedKeepDuration = .general
case .shortLived:
mappedKeepDuration = .shortLived
}
return self.account.postbox.mediaBox.customResourceData(
id: id,
baseResourceId: nil,
pathExtension: nil,
complete: true,
fetch: fetch.flatMap { fetch in
return {
return Signal { subscriber in
return fetch.signal().start(next: { result in
let mappedResult: CachedMediaResourceRepresentationResult
switch result {
case let .moveTempFile(file):
mappedResult = .tempFile(file)
default:
assert(false)
return
}
subscriber.putNext(mappedResult)
}, completed: {
subscriber.putCompletion()
})
}
}
},
keepDuration: mappedKeepDuration,
attemptSynchronously: attemptSynchronously
)
|> map { data in
return EngineMediaResource.ResourceData(data)
}
}
public func httpData(url: String, preserveExactUrl: Bool = false) -> Signal<Data, EngineMediaResource.Fetch.Error> {
return fetchHttpResource(url: url, preserveExactUrl: preserveExactUrl)
|> mapError { _ -> EngineMediaResource.Fetch.Error in
return .generic
}
|> mapToSignal { value -> Signal<Data, EngineMediaResource.Fetch.Error> in
switch value {
case let .dataPart(_, data, _, _):
return .single(data)
default:
return .complete()
}
}
}
public func fetchAlbumCover(file: FileMediaReference?, title: String, performer: String, isThumbnail: Bool) -> Signal<EngineMediaResource.Fetch.Result, EngineMediaResource.Fetch.Error> {
let signal = currentWebDocumentsHostDatacenterId(postbox: self.account.postbox, isTestingEnvironment: self.account.testingEnvironment)
|> castError(EngineMediaResource.Fetch.Error.self)
|> take(1)
|> mapToSignal { datacenterId -> Signal<EngineMediaResource.Fetch.Result, EngineMediaResource.Fetch.Error> in
let resource = AlbumCoverResource(datacenterId: Int(datacenterId), file: file, title: title, performer: performer, isThumbnail: isThumbnail)
return multipartFetch(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, mediaReferenceRevalidationContext: self.account.mediaReferenceRevalidationContext, networkStatsContext: self.account.networkStatsContext, resource: resource, datacenterId: Int(datacenterId), size: nil, intervals: .single([(0 ..< Int64.max, .default)]), parameters: MediaResourceFetchParameters(
tag: nil,
info: TelegramCloudMediaResourceFetchInfo(
reference: MediaResourceReference.standalone(resource: resource),
preferBackgroundReferenceRevalidation: false,
continueInBackground: false
),
location: nil,
contentType: .image,
isRandomAccessAllowed: true
))
|> map { result -> EngineMediaResource.Fetch.Result in
switch result {
case let .dataPart(resourceOffset, data, range, complete):
return .dataPart(resourceOffset: resourceOffset, data: data, range: range, complete: complete)
case let .resourceSizeUpdated(size):
return .resourceSizeUpdated(size)
case let .progressUpdated(value):
return .progressUpdated(value)
case let .replaceHeader(data, range):
return .replaceHeader(data: data, range: range)
case let .moveLocalFile(path):
return .moveLocalFile(path: path)
case let .moveTempFile(file):
return .moveTempFile(file: file)
case let .copyLocalItem(item):
return .copyLocalItem(item)
case .reset:
return .reset
}
}
|> mapError { error -> EngineMediaResource.Fetch.Error in
switch error {
case .generic:
return .generic
}
}
}
return bufferedFetch(signal)
}
public func cancelAllFetches(id: String) {
preconditionFailure()
}
public func pushPriorityDownload(resourceId: String, priority: Int = 1) -> Disposable {
return self.account.network.multiplexedRequestManager.pushPriority(resourceId: resourceId, priority: priority)
}
public func applicationIcons() -> Signal<TelegramApplicationIcons, NoError> {
return _internal_applicationIcons(account: account)
}
}
}