Channels caching

This commit is contained in:
Arkadiusz Fal 2022-12-14 00:07:32 +01:00
parent d9622cf24c
commit 3b31f21c81
18 changed files with 151 additions and 18 deletions

View File

@ -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: "")
)
}
}

View File

@ -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)!,

View File

@ -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)
)
}

View File

@ -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)
)
}

View File

@ -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)
)
}

View File

@ -14,6 +14,7 @@ struct BaseCacheModel {
[
FeedCacheModel.shared,
VideosCacheModel.shared,
ChannelsCacheModel.shared,
PlaylistsCacheModel.shared,
ChannelPlaylistsCacheModel.shared,
SubscribedChannelsModel.shared

View 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
}
}

View File

@ -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 []

View File

@ -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? {

View File

@ -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

View File

@ -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? {

View File

@ -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

View File

@ -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()

View File

@ -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:

View File

@ -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))

View File

@ -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())
}

View File

@ -166,9 +166,19 @@ struct VideoCell: View {
videoDetail(video.displayTitle, lineLimit: 5)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
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 {
Spacer()
@ -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()
}

View File

@ -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 */,