mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 05:23:41 +00:00
Channels caching
This commit is contained in:
parent
d9622cf24c
commit
3b31f21c81
@ -12,7 +12,7 @@ extension Comment {
|
||||
likeCount: 30032,
|
||||
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut luctus feugiat mi, suscipit pharetra lectus dapibus vel. Vivamus orci erat, sagittis sit amet dui vel, feugiat cursus ante. Pellentesque eget orci tortor. Suspendisse pulvinar orci tortor, eu scelerisque neque consequat nec. Aliquam sit amet turpis et nunc placerat finibus eget sit amet justo. Nullam tincidunt ornare neque. Donec ornare, arcu at elementum pulvinar, urna elit pharetra diam, vel ultrices lacus diam at lorem. Sed vel maximus dolor. Morbi massa est, interdum quis justo sit amet, dapibus bibendum tellus. Integer at purus nec neque tincidunt convallis sit amet eu odio. Duis et ante vitae sem tincidunt facilisis sit amet ac mauris. Quisque varius non nisi vel placerat. Nulla orci metus, imperdiet ac accumsan sed, pellentesque eget nisl. Praesent a suscipit lacus, ut finibus orci. Nulla ut eros commodo, fermentum purus at, porta leo. In finibus luctus nulla, eget posuere eros mollis vel. ",
|
||||
repliesPage: "some url",
|
||||
channel: .init(id: "", name: "")
|
||||
channel: .init(app: .invidious, id: "", name: "")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ extension Video {
|
||||
description: "Some relaxing live piano music",
|
||||
genre: "Music",
|
||||
channel: Channel(
|
||||
app: .invidious,
|
||||
id: fixtureChannelID,
|
||||
name: "The Channel",
|
||||
bannerURL: URL(string: bannerURL)!,
|
||||
|
@ -519,6 +519,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
}
|
||||
|
||||
return Channel(
|
||||
app: .invidious,
|
||||
id: json["authorId"].stringValue,
|
||||
name: json["author"].stringValue,
|
||||
bannerURL: json["authorBanners"].arrayValue.first?.dictionaryValue["url"]?.url,
|
||||
@ -666,7 +667,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
likeCount: details["likeCount"]?.int ?? 0,
|
||||
text: details["content"]?.string ?? "",
|
||||
repliesPage: details["replies"]?.dictionaryValue["continuation"]?.string,
|
||||
channel: Channel(id: channelId, name: author)
|
||||
channel: Channel(app: .invidious, id: channelId, name: author)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -472,6 +472,7 @@ final class PeerTubeAPI: Service, ObservableObject, VideosAPI {
|
||||
|
||||
func extractChannel(from json: JSON) -> Channel {
|
||||
Channel(
|
||||
app: .peerTube,
|
||||
id: json["id"].stringValue,
|
||||
name: json["name"].stringValue
|
||||
)
|
||||
@ -572,7 +573,7 @@ final class PeerTubeAPI: Service, ObservableObject, VideosAPI {
|
||||
likeCount: details["likeCount"]?.int ?? 0,
|
||||
text: details["content"]?.string ?? "",
|
||||
repliesPage: details["replies"]?.dictionaryValue["continuation"]?.string,
|
||||
channel: Channel(id: channelId, name: author)
|
||||
channel: Channel(app: .peerTube, id: channelId, name: author)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -410,6 +410,7 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
||||
} ?? [Channel.Tab]()
|
||||
|
||||
return Channel(
|
||||
app: .piped,
|
||||
id: id,
|
||||
name: name,
|
||||
bannerURL: attributes["bannerUrl"]?.url,
|
||||
@ -488,7 +489,7 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
||||
published: published ?? "",
|
||||
views: details["views"]?.int ?? 0,
|
||||
description: description,
|
||||
channel: Channel(id: channelId, name: author, thumbnailURL: authorThumbnailURL, subscriptionsCount: subscriptionsCount),
|
||||
channel: Channel(app: .piped, id: channelId, name: author, thumbnailURL: authorThumbnailURL, subscriptionsCount: subscriptionsCount),
|
||||
thumbnails: thumbnails,
|
||||
live: live,
|
||||
likes: details["likes"]?.int,
|
||||
@ -667,7 +668,7 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
|
||||
likeCount: details["likeCount"]?.int ?? 0,
|
||||
text: extractCommentText(from: details["commentText"]?.stringValue),
|
||||
repliesPage: details["repliesPage"]?.string,
|
||||
channel: Channel(id: channelId, name: author)
|
||||
channel: Channel(app: .piped, id: channelId, name: author)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,7 @@ struct BaseCacheModel {
|
||||
[
|
||||
FeedCacheModel.shared,
|
||||
VideosCacheModel.shared,
|
||||
ChannelsCacheModel.shared,
|
||||
PlaylistsCacheModel.shared,
|
||||
ChannelPlaylistsCacheModel.shared,
|
||||
SubscribedChannelsModel.shared
|
||||
|
46
Model/Cache/ChannelsCacheModel.swift
Normal file
46
Model/Cache/ChannelsCacheModel.swift
Normal file
@ -0,0 +1,46 @@
|
||||
import Cache
|
||||
import Foundation
|
||||
import Logging
|
||||
import SwiftyJSON
|
||||
|
||||
struct ChannelsCacheModel: CacheModel {
|
||||
static let shared = ChannelsCacheModel()
|
||||
let logger = Logger(label: "stream.yattee.cache.channels")
|
||||
|
||||
static let diskConfig = DiskConfig(name: "channels")
|
||||
static let memoryConfig = MemoryConfig()
|
||||
|
||||
let storage = try? Storage<String, JSON>(
|
||||
diskConfig: Self.diskConfig,
|
||||
memoryConfig: Self.memoryConfig,
|
||||
transformer: BaseCacheModel.jsonTransformer
|
||||
)
|
||||
|
||||
func store(_ channel: Channel) {
|
||||
guard channel.hasExtendedDetails else {
|
||||
logger.warning("not caching \(channel.cacheKey)")
|
||||
return
|
||||
}
|
||||
|
||||
logger.info("caching \(channel.cacheKey)")
|
||||
try? storage?.setObject(channel.json, forKey: channel.cacheKey)
|
||||
}
|
||||
|
||||
func storeIfMissing(_ channel: Channel) {
|
||||
guard let storage, !storage.objectExists(forKey: channel.cacheKey) else {
|
||||
return
|
||||
}
|
||||
|
||||
store(channel)
|
||||
}
|
||||
|
||||
func retrieve(_ cacheKey: String) -> Channel? {
|
||||
logger.info("retrieving cache for \(cacheKey)")
|
||||
|
||||
if let json = try? storage?.object(forKey: cacheKey) {
|
||||
return Channel.from(json)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
@ -69,6 +69,7 @@ final class SubscribedChannelsModel: ObservableObject, CacheModel {
|
||||
.onSuccess { resource in
|
||||
if let channels: [Channel] = resource.typedContent() {
|
||||
self.channels = channels
|
||||
channels.forEach { ChannelsCacheModel.shared.storeIfMissing($0) }
|
||||
self.storeChannels(account: account, channels: channels)
|
||||
FeedModel.shared.calculateUnwatchedFeed()
|
||||
onSuccess()
|
||||
@ -93,6 +94,8 @@ final class SubscribedChannelsModel: ObservableObject, CacheModel {
|
||||
let date = iso8601DateFormatter.string(from: Date())
|
||||
logger.info("caching channels \(channelsDateCacheKey(account)) -- \(date)")
|
||||
|
||||
channels.forEach { ChannelsCacheModel.shared.storeIfMissing($0) }
|
||||
|
||||
let dateObject: JSON = ["date": date]
|
||||
let channelsObject: JSON = ["channels": channels.map(\.json).map(\.object)]
|
||||
|
||||
@ -106,7 +109,16 @@ final class SubscribedChannelsModel: ObservableObject, CacheModel {
|
||||
if let json = try? storage?.object(forKey: channelsCacheKey(account)),
|
||||
let channels = json.dictionaryValue["channels"]
|
||||
{
|
||||
return channels.arrayValue.map { Channel.from($0) }
|
||||
return channels.arrayValue.map { json in
|
||||
let channel = Channel.from(json)
|
||||
if !channel.hasExtendedDetails,
|
||||
let cache = ChannelsCacheModel.shared.retrieve(channel.cacheKey)
|
||||
{
|
||||
return cache
|
||||
}
|
||||
|
||||
return channel
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
|
@ -19,6 +19,8 @@ struct VideosCacheModel: CacheModel {
|
||||
func storeVideo(_ video: Video) {
|
||||
logger.info("caching \(video.cacheKey)")
|
||||
try? storage?.setObject(video.json, forKey: video.cacheKey)
|
||||
|
||||
ChannelsCacheModel.shared.storeIfMissing(video.channel)
|
||||
}
|
||||
|
||||
func retrieveVideo(_ cacheKey: String) -> Video? {
|
||||
|
@ -64,6 +64,10 @@ struct Channel: Identifiable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
var app: VideosApp
|
||||
var instanceID: Instance.ID?
|
||||
var instanceURL: URL?
|
||||
|
||||
var id: String
|
||||
var name: String
|
||||
var bannerURL: URL?
|
||||
@ -112,14 +116,37 @@ struct Channel: Identifiable, Hashable {
|
||||
|
||||
var json: JSON {
|
||||
[
|
||||
"app": app.rawValue,
|
||||
"id": id,
|
||||
"name": name,
|
||||
"thumbnailURL": thumbnailURL?.absoluteString ?? ""
|
||||
]
|
||||
}
|
||||
|
||||
var cacheKey: String {
|
||||
switch app {
|
||||
case .local:
|
||||
return id
|
||||
case .invidious:
|
||||
return "youtube-\(id)"
|
||||
case .piped:
|
||||
return "youtube-\(id)"
|
||||
case .peerTube:
|
||||
return "peertube-\(instanceURL?.absoluteString ?? "unknown-instance")-\(id)"
|
||||
}
|
||||
}
|
||||
|
||||
var hasExtendedDetails: Bool {
|
||||
thumbnailURL != nil
|
||||
}
|
||||
|
||||
var thumbnailURLOrCached: URL? {
|
||||
thumbnailURL ?? ChannelsCacheModel.shared.retrieve(cacheKey)?.thumbnailURL
|
||||
}
|
||||
|
||||
static func from(_ json: JSON) -> Self {
|
||||
.init(
|
||||
app: VideosApp(rawValue: json["app"].stringValue) ?? .local,
|
||||
id: json["id"].stringValue,
|
||||
name: json["name"].stringValue,
|
||||
thumbnailURL: json["thumbnailURL"].url
|
||||
|
@ -98,7 +98,7 @@ struct RecentItem: Defaults.Serializable, Identifiable {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Channel(id: id, name: title)
|
||||
return Channel(app: .invidious, id: id, name: title)
|
||||
}
|
||||
|
||||
var playlist: ChannelPlaylist? {
|
||||
|
@ -69,7 +69,7 @@ struct Video: Identifiable, Equatable, Hashable {
|
||||
views: Int = 0,
|
||||
description: String? = nil,
|
||||
genre: String? = nil,
|
||||
channel: Channel = .init(id: "", name: ""),
|
||||
channel: Channel? = nil,
|
||||
thumbnails: [Thumbnail] = [],
|
||||
indexID: String? = nil,
|
||||
live: Bool = false,
|
||||
@ -96,7 +96,7 @@ struct Video: Identifiable, Equatable, Hashable {
|
||||
self.views = views
|
||||
self.description = description
|
||||
self.genre = genre
|
||||
self.channel = channel
|
||||
self.channel = channel ?? .init(app: app, id: "", name: "")
|
||||
self.thumbnails = thumbnails
|
||||
self.indexID = indexID
|
||||
self.live = live
|
||||
|
@ -133,7 +133,18 @@ struct ChannelVideosView: View {
|
||||
}
|
||||
#endif
|
||||
.onAppear {
|
||||
resource?.loadIfNeeded()
|
||||
if let channel,
|
||||
let cache = ChannelsCacheModel.shared.retrieve(channel.cacheKey),
|
||||
store.item.isNil
|
||||
{
|
||||
store.replace(cache)
|
||||
}
|
||||
|
||||
resource?.loadIfNeeded()?.onSuccess { response in
|
||||
if let channel: Channel = response.typedContent() {
|
||||
ChannelsCacheModel.shared.store(channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: contentType) { _ in
|
||||
resource?.load()
|
||||
|
@ -121,7 +121,7 @@ struct FavoriteItemView: View {
|
||||
Group {
|
||||
switch item.section {
|
||||
case let .channel(_, id, name):
|
||||
ChannelVideosView(channel: .init(id: id, name: name))
|
||||
ChannelVideosView(channel: .init(app: .invidious, id: id, name: name))
|
||||
case let .channelPlaylist(_, id, title):
|
||||
ChannelPlaylistView(playlist: .init(id: id, title: title))
|
||||
case let .playlist(_, id):
|
||||
@ -140,7 +140,7 @@ struct FavoriteItemView: View {
|
||||
func itemButtonAction() {
|
||||
switch item.section {
|
||||
case let .channel(_, id, name):
|
||||
NavigationModel.shared.openChannel(.init(id: id, name: name), navigationStyle: navigationStyle)
|
||||
NavigationModel.shared.openChannel(.init(app: .invidious, id: id, name: name), navigationStyle: navigationStyle)
|
||||
case let .channelPlaylist(_, id, title):
|
||||
NavigationModel.shared.openChannelPlaylist(.init(id: id, title: title), navigationStyle: navigationStyle)
|
||||
case .subscriptions:
|
||||
|
@ -15,7 +15,7 @@ struct ChannelsView: View {
|
||||
ForEach(subscriptions.all) { channel in
|
||||
NavigationLink(destination: ChannelVideosView(channel: channel).modifier(PlayerOverlayModifier())) {
|
||||
HStack {
|
||||
if let url = channel.thumbnailURL {
|
||||
if let url = channel.thumbnailURLOrCached {
|
||||
ThumbnailView(url: url)
|
||||
.frame(width: 35, height: 35)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 35))
|
||||
|
@ -74,8 +74,11 @@ struct VideoBanner: View {
|
||||
|
||||
HStack {
|
||||
HStack {
|
||||
if !inChannelView {
|
||||
ThumbnailView(url: video?.channel.thumbnailURL)
|
||||
if !inChannelView,
|
||||
let video,
|
||||
let url = video.channel.thumbnailURLOrCached
|
||||
{
|
||||
ThumbnailView(url: url)
|
||||
.frame(width: 30, height: 30)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
|
@ -166,8 +166,18 @@ struct VideoCell: View {
|
||||
videoDetail(video.displayTitle, lineLimit: 5)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
if !channelOnThumbnail, !inChannelView {
|
||||
channelControl(badge: false)
|
||||
HStack(spacing: 12) {
|
||||
if !inChannelView,
|
||||
let video,
|
||||
let url = video.channel.thumbnailURLOrCached
|
||||
{
|
||||
ThumbnailView(url: url)
|
||||
.frame(width: 30, height: 30)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
if !channelOnThumbnail, !inChannelView {
|
||||
channelControl(badge: false)
|
||||
}
|
||||
}
|
||||
|
||||
if additionalDetailsAvailable {
|
||||
@ -271,6 +281,15 @@ struct VideoCell: View {
|
||||
.padding(.bottom, 4)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
if !inChannelView,
|
||||
let video,
|
||||
let url = video.channel.thumbnailURLOrCached
|
||||
{
|
||||
ThumbnailView(url: url)
|
||||
.frame(width: 30, height: 30)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
|
||||
if let date = video.publishedDate {
|
||||
HStack(spacing: 2) {
|
||||
Text(date)
|
||||
@ -512,7 +531,7 @@ struct VideoCell_Preview: PreviewProvider {
|
||||
#if os(macOS)
|
||||
.frame(maxWidth: 300, maxHeight: 250)
|
||||
#elseif os(iOS)
|
||||
.frame(maxWidth: 300, maxHeight: 200)
|
||||
.frame(maxWidth: 600, maxHeight: 200)
|
||||
#endif
|
||||
.injectFixtureEnvironmentObjects()
|
||||
}
|
||||
|
@ -849,6 +849,9 @@
|
||||
37D6025A28C17375009E8D98 /* PlaybackStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D6025828C17375009E8D98 /* PlaybackStatsView.swift */; };
|
||||
37D6025B28C17375009E8D98 /* PlaybackStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D6025828C17375009E8D98 /* PlaybackStatsView.swift */; };
|
||||
37D6025D28C17719009E8D98 /* ControlsOverlayButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D6025C28C17719009E8D98 /* ControlsOverlayButton.swift */; };
|
||||
37D836BC294927E700005E5E /* ChannelsCacheModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D836BB294927E700005E5E /* ChannelsCacheModel.swift */; };
|
||||
37D836BD294927E700005E5E /* ChannelsCacheModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D836BB294927E700005E5E /* ChannelsCacheModel.swift */; };
|
||||
37D836BE294927E700005E5E /* ChannelsCacheModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D836BB294927E700005E5E /* ChannelsCacheModel.swift */; };
|
||||
37DA0F20291DD6B8009B38CF /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 37DA0F1F291DD6B8009B38CF /* Logging */; };
|
||||
37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */; };
|
||||
37DD87C8271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */; };
|
||||
@ -1410,6 +1413,7 @@
|
||||
37D526DD2720AC4400ED2F5E /* VideosAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideosAPI.swift; sourceTree = "<group>"; };
|
||||
37D6025828C17375009E8D98 /* PlaybackStatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackStatsView.swift; sourceTree = "<group>"; };
|
||||
37D6025C28C17719009E8D98 /* ControlsOverlayButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlsOverlayButton.swift; sourceTree = "<group>"; };
|
||||
37D836BB294927E700005E5E /* ChannelsCacheModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelsCacheModel.swift; sourceTree = "<group>"; };
|
||||
37D9169A27388A81002B1BAA /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||
37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerStreams.swift; sourceTree = "<group>"; };
|
||||
37DD9DA22785BBC900539416 /* NoCommentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoCommentsView.swift; sourceTree = "<group>"; };
|
||||
@ -2060,6 +2064,7 @@
|
||||
3738535329451DC800D2D0CB /* BookmarksCacheModel.swift */,
|
||||
37A2B345294723850050933E /* CacheModel.swift */,
|
||||
377692552946476F0055EC18 /* ChannelPlaylistsCacheModel.swift */,
|
||||
37D836BB294927E700005E5E /* ChannelsCacheModel.swift */,
|
||||
377F9F7E2944175F0043F856 /* FeedCacheModel.swift */,
|
||||
3776925129463C310055EC18 /* PlaylistsCacheModel.swift */,
|
||||
37E64DD026D597EB00C71877 /* SubscribedChannelsModel.swift */,
|
||||
@ -3052,6 +3057,7 @@
|
||||
376A33E02720CAD6000C1D6B /* VideosApp.swift in Sources */,
|
||||
374AB3DB28BCAF7E00DF56FB /* SeekType.swift in Sources */,
|
||||
37192D5728B179D60012EEDD /* ChaptersView.swift in Sources */,
|
||||
37D836BC294927E700005E5E /* ChannelsCacheModel.swift in Sources */,
|
||||
37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */,
|
||||
37C0698227260B2100F7F6CB /* ThumbnailsModel.swift in Sources */,
|
||||
37BC50A82778A84700510953 /* HistorySettings.swift in Sources */,
|
||||
@ -3254,6 +3260,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
37D836BD294927E700005E5E /* ChannelsCacheModel.swift in Sources */,
|
||||
3727B74B27872B880021C15E /* VisualEffectBlur-macOS.swift in Sources */,
|
||||
374710062755291C00CE0F87 /* SearchTextField.swift in Sources */,
|
||||
37F0F4EB286F397E00C06C2E /* SettingsModel.swift in Sources */,
|
||||
@ -3753,6 +3760,7 @@
|
||||
37141675267A8E10006CA35D /* Country.swift in Sources */,
|
||||
370F500C27CC1821001B35DC /* MPVViewController.swift in Sources */,
|
||||
3782B9542755667600990149 /* String+Format.swift in Sources */,
|
||||
37D836BE294927E700005E5E /* ChannelsCacheModel.swift in Sources */,
|
||||
37152EEC26EFEB95004FB96D /* LazyView.swift in Sources */,
|
||||
37EF9A78275BEB8E0043B585 /* CommentView.swift in Sources */,
|
||||
37484C2726FC83E000287258 /* InstanceForm.swift in Sources */,
|
||||
|
Loading…
Reference in New Issue
Block a user