Feed cache

This commit is contained in:
Arkadiusz Fal
2022-12-10 03:01:59 +01:00
parent eae04c9382
commit 971beddc8d
24 changed files with 484 additions and 237 deletions

View File

@@ -84,4 +84,8 @@ struct Account: Defaults.Serializable, Hashable, Identifiable {
func hash(into hasher: inout Hasher) {
hasher.combine(username)
}
var feedCacheKey: String {
"feed-\(id)"
}
}

View File

@@ -9,7 +9,6 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
static let basePath = "/api/v1"
@Published var account: Account!
@Published var validInstance = true
static func withAnonymousAccountForInstanceURL(_ url: URL) -> InvidiousAPI {
.init(account: Instance(app: .invidious, apiURLString: url.absoluteString).anonymousAccount)
@@ -35,8 +34,6 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
func setAccount(_ account: Account) {
self.account = account
validInstance = account.anonymous
configure()
if !account.anonymous {
@@ -45,31 +42,15 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
}
func validate() {
validateInstance()
validateSID()
}
func validateInstance() {
guard !validInstance else {
return
}
home?
.load()
.onSuccess { _ in
self.validInstance = true
}
.onFailure { _ in
self.validInstance = false
}
}
func validateSID() {
guard signedIn, !(account.token?.isEmpty ?? true) else {
return
}
feed?
notifications?
.load()
.onFailure { _ in
self.updateToken(force: true)
@@ -273,8 +254,17 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
resource(baseURL: account.url, path: "/feed/subscriptions")
}
var feed: Resource? {
func feed(_ page: Int?) -> Resource? {
resource(baseURL: account.url, path: "\(Self.basePath)/auth/feed")
.withParam("page", String(page ?? 1))
}
var feed: Resource? {
resource(baseURL: account.url, path: basePathAppending("auth/feed"))
}
var notifications: Resource? {
resource(baseURL: account.url, path: basePathAppending("auth/notifications"))
}
var subscriptions: Resource? {

View File

@@ -70,7 +70,7 @@ final class PeerTubeAPI: Service, ObservableObject, VideosAPI {
return
}
feed?
feed(1)?
.load()
.onFailure { _ in
self.updateToken(force: true)
@@ -262,8 +262,9 @@ final class PeerTubeAPI: Service, ObservableObject, VideosAPI {
resource(baseURL: account.url, path: "/feed/subscriptions")
}
var feed: Resource? {
func feed(_ page: Int?) -> Resource? {
resource(baseURL: account.url, path: "\(Self.basePath)/auth/feed")
.withParam("page", String(page ?? 1))
}
var subscriptions: Resource? {

View File

@@ -220,7 +220,7 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
resource(baseURL: account.instance.apiURL, path: "subscriptions")
}
var feed: Resource? {
func feed(_: Int?) -> Resource? {
resource(baseURL: account.instance.apiURL, path: "feed")
.withParam("authToken", account.token)
}

View File

@@ -18,8 +18,8 @@ protocol VideosAPI {
func video(_ id: Video.ID) -> Resource
func feed(_ page: Int?) -> Resource?
var subscriptions: Resource? { get }
var feed: Resource? { get }
var home: Resource? { get }
var popular: Resource? { get }
var playlists: Resource? { get }

View File

@@ -30,6 +30,10 @@ enum VideosApp: String, CaseIterable {
supportsAccounts
}
var paginatesSubscriptions: Bool {
self == .invidious
}
var supportsTrendingCategories: Bool {
self == .invidious
}

View File

@@ -1,16 +1,40 @@
import Cache
import Foundation
import Logging
import SwiftyJSON
struct CacheModel {
static var shared = CacheModel()
static let jsonToDataTransformer: (JSON) -> Data = { try! $0.rawData() }
static let jsonFromDataTransformer: (Data) -> JSON = { try! JSON(data: $0) }
static let jsonTransformer = Transformer(toData: jsonToDataTransformer, fromData: jsonFromDataTransformer)
let logger = Logger(label: "stream.yattee.cache")
static let bookmarksGroup = "group.stream.yattee.app.bookmarks"
let bookmarksDefaults = UserDefaults(suiteName: Self.bookmarksGroup)
func removeAll() {
func clearBookmarks() {
guard let bookmarksDefaults else { return }
bookmarksDefaults.dictionaryRepresentation().keys.forEach(bookmarksDefaults.removeObject(forKey:))
}
func clear() {
FeedCacheModel.shared.clear()
VideosCacheModel.shared.clear()
}
var totalSize: Int {
(FeedCacheModel.shared.storage.totalDiskStorageSize ?? 0) +
(VideosCacheModel.shared.storage.totalDiskStorageSize ?? 0)
}
var totalSizeFormatted: String {
totalSizeFormatter.string(fromByteCount: Int64(totalSize))
}
private var totalSizeFormatter: ByteCountFormatter {
.init()
}
}

View File

@@ -0,0 +1,62 @@
import Cache
import Foundation
import Logging
import SwiftyJSON
struct FeedCacheModel {
static let shared = FeedCacheModel()
let logger = Logger(label: "stream.yattee.cache.feed")
static let diskConfig = DiskConfig(name: "feed")
static let memoryConfig = MemoryConfig()
let storage = try! Storage<String, JSON>(
diskConfig: Self.diskConfig,
memoryConfig: Self.memoryConfig,
transformer: CacheModel.jsonTransformer
)
func storeFeed(account: Account, videos: [Video]) {
let date = dateFormatter.string(from: Date())
logger.info("caching feed \(account.feedCacheKey) -- \(date)")
let feedTimeObject: JSON = ["date": date]
let videosObject: JSON = ["videos": videos.map(\.json).map(\.object)]
try? storage.setObject(feedTimeObject, forKey: feedTimeCacheKey(account.feedCacheKey))
try? storage.setObject(videosObject, forKey: account.feedCacheKey)
}
func retrieveFeed(account: Account) -> [Video] {
logger.info("retrieving cache for \(account.feedCacheKey)")
if let json = try? storage.object(forKey: account.feedCacheKey),
let videos = json.dictionaryValue["videos"]
{
return videos.arrayValue.map { Video.from($0) }
}
return []
}
func getFeedTime(account: Account) -> Date? {
if let json = try? storage.object(forKey: feedTimeCacheKey(account.feedCacheKey)),
let string = json.dictionaryValue["date"]?.string,
let date = dateFormatter.date(from: string)
{
return date
}
return nil
}
func clear() {
try? storage.removeAll()
}
private var dateFormatter: ISO8601DateFormatter {
.init()
}
private func feedTimeCacheKey(_ feedCacheKey: String) -> String {
"\(feedCacheKey)-feedTime"
}
}

View File

@@ -7,31 +7,31 @@ struct VideosCacheModel {
static let shared = VideosCacheModel()
let logger = Logger(label: "stream.yattee.cache.videos")
static let jsonToDataTransformer: (JSON) -> Data = { try! $0.rawData() }
static let jsonFromDataTransformer: (Data) -> JSON = { try! JSON(data: $0) }
static let jsonTransformer = Transformer(toData: jsonToDataTransformer, fromData: jsonFromDataTransformer)
static let diskConfig = DiskConfig(name: "videos")
static let memoryConfig = MemoryConfig()
static let videosStorageDiskConfig = DiskConfig(name: "videos")
static let vidoesStorageMemoryConfig = MemoryConfig()
let videosStorage = try! Storage<String, JSON>(
diskConfig: Self.videosStorageDiskConfig,
memoryConfig: Self.vidoesStorageMemoryConfig,
transformer: Self.jsonTransformer
let storage = try! Storage<String, JSON>(
diskConfig: Self.diskConfig,
memoryConfig: Self.memoryConfig,
transformer: CacheModel.jsonTransformer
)
func storeVideo(_ video: Video) {
logger.info("caching \(video.cacheKey)")
try? videosStorage.setObject(video.json, forKey: video.cacheKey)
try? storage.setObject(video.json, forKey: video.cacheKey)
}
func retrieveVideo(_ cacheKey: String) -> Video? {
logger.info("retrieving cache for \(cacheKey)")
if let json = try? videosStorage.object(forKey: cacheKey) {
if let json = try? storage.object(forKey: cacheKey) {
return Video.from(json)
}
return nil
}
func clear() {
try? storage.removeAll()
}
}

View File

@@ -103,7 +103,7 @@ extension PlayerModel {
func removeHistory() {
removeAllWatches()
CacheModel.shared.removeAll()
CacheModel.shared.clearBookmarks()
}
func removeWatch(_ watch: Watch) {

View File

@@ -279,7 +279,7 @@ extension PlayerModel {
}
func loadQueueVideoDetails(_ item: PlayerQueueItem) {
guard !accounts.current.isNil, !item.hasDetailsLoaded else { return }
guard !accounts.current.isNil, !item.hasDetailsLoaded, let video = item.video else { return }
let videoID = item.video?.videoID ?? item.videoID
@@ -292,7 +292,7 @@ extension PlayerModel {
return
}
playerAPI(item.video).loadDetails(item, completionHandler: { [weak self] newItem in
playerAPI(video)?.loadDetails(item, completionHandler: { [weak self] newItem in
guard let self else { return }
self.queue.filter { $0.videoID == item.videoID }.forEach { item in