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, 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. ", 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", 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", description: "Some relaxing live piano music",
genre: "Music", genre: "Music",
channel: Channel( channel: Channel(
app: .invidious,
id: fixtureChannelID, id: fixtureChannelID,
name: "The Channel", name: "The Channel",
bannerURL: URL(string: bannerURL)!, bannerURL: URL(string: bannerURL)!,

View File

@ -519,6 +519,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
} }
return Channel( return Channel(
app: .invidious,
id: json["authorId"].stringValue, id: json["authorId"].stringValue,
name: json["author"].stringValue, name: json["author"].stringValue,
bannerURL: json["authorBanners"].arrayValue.first?.dictionaryValue["url"]?.url, bannerURL: json["authorBanners"].arrayValue.first?.dictionaryValue["url"]?.url,
@ -666,7 +667,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
likeCount: details["likeCount"]?.int ?? 0, likeCount: details["likeCount"]?.int ?? 0,
text: details["content"]?.string ?? "", text: details["content"]?.string ?? "",
repliesPage: details["replies"]?.dictionaryValue["continuation"]?.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 { func extractChannel(from json: JSON) -> Channel {
Channel( Channel(
app: .peerTube,
id: json["id"].stringValue, id: json["id"].stringValue,
name: json["name"].stringValue name: json["name"].stringValue
) )
@ -572,7 +573,7 @@ final class PeerTubeAPI: Service, ObservableObject, VideosAPI {
likeCount: details["likeCount"]?.int ?? 0, likeCount: details["likeCount"]?.int ?? 0,
text: details["content"]?.string ?? "", text: details["content"]?.string ?? "",
repliesPage: details["replies"]?.dictionaryValue["continuation"]?.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]() } ?? [Channel.Tab]()
return Channel( return Channel(
app: .piped,
id: id, id: id,
name: name, name: name,
bannerURL: attributes["bannerUrl"]?.url, bannerURL: attributes["bannerUrl"]?.url,
@ -488,7 +489,7 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
published: published ?? "", published: published ?? "",
views: details["views"]?.int ?? 0, views: details["views"]?.int ?? 0,
description: description, 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, thumbnails: thumbnails,
live: live, live: live,
likes: details["likes"]?.int, likes: details["likes"]?.int,
@ -667,7 +668,7 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
likeCount: details["likeCount"]?.int ?? 0, likeCount: details["likeCount"]?.int ?? 0,
text: extractCommentText(from: details["commentText"]?.stringValue), text: extractCommentText(from: details["commentText"]?.stringValue),
repliesPage: details["repliesPage"]?.string, 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, FeedCacheModel.shared,
VideosCacheModel.shared, VideosCacheModel.shared,
ChannelsCacheModel.shared,
PlaylistsCacheModel.shared, PlaylistsCacheModel.shared,
ChannelPlaylistsCacheModel.shared, ChannelPlaylistsCacheModel.shared,
SubscribedChannelsModel.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 .onSuccess { resource in
if let channels: [Channel] = resource.typedContent() { if let channels: [Channel] = resource.typedContent() {
self.channels = channels self.channels = channels
channels.forEach { ChannelsCacheModel.shared.storeIfMissing($0) }
self.storeChannels(account: account, channels: channels) self.storeChannels(account: account, channels: channels)
FeedModel.shared.calculateUnwatchedFeed() FeedModel.shared.calculateUnwatchedFeed()
onSuccess() onSuccess()
@ -93,6 +94,8 @@ final class SubscribedChannelsModel: ObservableObject, CacheModel {
let date = iso8601DateFormatter.string(from: Date()) let date = iso8601DateFormatter.string(from: Date())
logger.info("caching channels \(channelsDateCacheKey(account)) -- \(date)") logger.info("caching channels \(channelsDateCacheKey(account)) -- \(date)")
channels.forEach { ChannelsCacheModel.shared.storeIfMissing($0) }
let dateObject: JSON = ["date": date] let dateObject: JSON = ["date": date]
let channelsObject: JSON = ["channels": channels.map(\.json).map(\.object)] 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)), if let json = try? storage?.object(forKey: channelsCacheKey(account)),
let channels = json.dictionaryValue["channels"] 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 [] return []

View File

@ -19,6 +19,8 @@ struct VideosCacheModel: CacheModel {
func storeVideo(_ video: Video) { func storeVideo(_ video: Video) {
logger.info("caching \(video.cacheKey)") logger.info("caching \(video.cacheKey)")
try? storage?.setObject(video.json, forKey: video.cacheKey) try? storage?.setObject(video.json, forKey: video.cacheKey)
ChannelsCacheModel.shared.storeIfMissing(video.channel)
} }
func retrieveVideo(_ cacheKey: String) -> Video? { 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 id: String
var name: String var name: String
var bannerURL: URL? var bannerURL: URL?
@ -112,14 +116,37 @@ struct Channel: Identifiable, Hashable {
var json: JSON { var json: JSON {
[ [
"app": app.rawValue,
"id": id, "id": id,
"name": name, "name": name,
"thumbnailURL": thumbnailURL?.absoluteString ?? "" "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 { static func from(_ json: JSON) -> Self {
.init( .init(
app: VideosApp(rawValue: json["app"].stringValue) ?? .local,
id: json["id"].stringValue, id: json["id"].stringValue,
name: json["name"].stringValue, name: json["name"].stringValue,
thumbnailURL: json["thumbnailURL"].url thumbnailURL: json["thumbnailURL"].url

View File

@ -98,7 +98,7 @@ struct RecentItem: Defaults.Serializable, Identifiable {
return nil return nil
} }
return Channel(id: id, name: title) return Channel(app: .invidious, id: id, name: title)
} }
var playlist: ChannelPlaylist? { var playlist: ChannelPlaylist? {

View File

@ -69,7 +69,7 @@ struct Video: Identifiable, Equatable, Hashable {
views: Int = 0, views: Int = 0,
description: String? = nil, description: String? = nil,
genre: String? = nil, genre: String? = nil,
channel: Channel = .init(id: "", name: ""), channel: Channel? = nil,
thumbnails: [Thumbnail] = [], thumbnails: [Thumbnail] = [],
indexID: String? = nil, indexID: String? = nil,
live: Bool = false, live: Bool = false,
@ -96,7 +96,7 @@ struct Video: Identifiable, Equatable, Hashable {
self.views = views self.views = views
self.description = description self.description = description
self.genre = genre self.genre = genre
self.channel = channel self.channel = channel ?? .init(app: app, id: "", name: "")
self.thumbnails = thumbnails self.thumbnails = thumbnails
self.indexID = indexID self.indexID = indexID
self.live = live self.live = live

View File

@ -133,7 +133,18 @@ struct ChannelVideosView: View {
} }
#endif #endif
.onAppear { .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 .onChange(of: contentType) { _ in
resource?.load() resource?.load()

View File

@ -121,7 +121,7 @@ struct FavoriteItemView: View {
Group { Group {
switch item.section { switch item.section {
case let .channel(_, id, name): 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): case let .channelPlaylist(_, id, title):
ChannelPlaylistView(playlist: .init(id: id, title: title)) ChannelPlaylistView(playlist: .init(id: id, title: title))
case let .playlist(_, id): case let .playlist(_, id):
@ -140,7 +140,7 @@ struct FavoriteItemView: View {
func itemButtonAction() { func itemButtonAction() {
switch item.section { switch item.section {
case let .channel(_, id, name): 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): case let .channelPlaylist(_, id, title):
NavigationModel.shared.openChannelPlaylist(.init(id: id, title: title), navigationStyle: navigationStyle) NavigationModel.shared.openChannelPlaylist(.init(id: id, title: title), navigationStyle: navigationStyle)
case .subscriptions: case .subscriptions:

View File

@ -15,7 +15,7 @@ struct ChannelsView: View {
ForEach(subscriptions.all) { channel in ForEach(subscriptions.all) { channel in
NavigationLink(destination: ChannelVideosView(channel: channel).modifier(PlayerOverlayModifier())) { NavigationLink(destination: ChannelVideosView(channel: channel).modifier(PlayerOverlayModifier())) {
HStack { HStack {
if let url = channel.thumbnailURL { if let url = channel.thumbnailURLOrCached {
ThumbnailView(url: url) ThumbnailView(url: url)
.frame(width: 35, height: 35) .frame(width: 35, height: 35)
.clipShape(RoundedRectangle(cornerRadius: 35)) .clipShape(RoundedRectangle(cornerRadius: 35))

View File

@ -74,8 +74,11 @@ struct VideoBanner: View {
HStack { HStack {
HStack { HStack {
if !inChannelView { if !inChannelView,
ThumbnailView(url: video?.channel.thumbnailURL) let video,
let url = video.channel.thumbnailURLOrCached
{
ThumbnailView(url: url)
.frame(width: 30, height: 30) .frame(width: 30, height: 30)
.clipShape(Circle()) .clipShape(Circle())
} }

View File

@ -166,8 +166,18 @@ struct VideoCell: View {
videoDetail(video.displayTitle, lineLimit: 5) videoDetail(video.displayTitle, lineLimit: 5)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
if !channelOnThumbnail, !inChannelView { HStack(spacing: 12) {
channelControl(badge: false) 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 { if additionalDetailsAvailable {
@ -271,6 +281,15 @@ struct VideoCell: View {
.padding(.bottom, 4) .padding(.bottom, 4)
HStack(spacing: 8) { 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 { if let date = video.publishedDate {
HStack(spacing: 2) { HStack(spacing: 2) {
Text(date) Text(date)
@ -512,7 +531,7 @@ struct VideoCell_Preview: PreviewProvider {
#if os(macOS) #if os(macOS)
.frame(maxWidth: 300, maxHeight: 250) .frame(maxWidth: 300, maxHeight: 250)
#elseif os(iOS) #elseif os(iOS)
.frame(maxWidth: 300, maxHeight: 200) .frame(maxWidth: 600, maxHeight: 200)
#endif #endif
.injectFixtureEnvironmentObjects() .injectFixtureEnvironmentObjects()
} }

View File

@ -849,6 +849,9 @@
37D6025A28C17375009E8D98 /* PlaybackStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D6025828C17375009E8D98 /* PlaybackStatsView.swift */; }; 37D6025A28C17375009E8D98 /* PlaybackStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D6025828C17375009E8D98 /* PlaybackStatsView.swift */; };
37D6025B28C17375009E8D98 /* 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 */; }; 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 */; }; 37DA0F20291DD6B8009B38CF /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 37DA0F1F291DD6B8009B38CF /* Logging */; };
37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */; }; 37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */; };
37DD87C8271C9CFE0027CBF9 /* 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 37DD9DA22785BBC900539416 /* NoCommentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoCommentsView.swift; sourceTree = "<group>"; };
@ -2060,6 +2064,7 @@
3738535329451DC800D2D0CB /* BookmarksCacheModel.swift */, 3738535329451DC800D2D0CB /* BookmarksCacheModel.swift */,
37A2B345294723850050933E /* CacheModel.swift */, 37A2B345294723850050933E /* CacheModel.swift */,
377692552946476F0055EC18 /* ChannelPlaylistsCacheModel.swift */, 377692552946476F0055EC18 /* ChannelPlaylistsCacheModel.swift */,
37D836BB294927E700005E5E /* ChannelsCacheModel.swift */,
377F9F7E2944175F0043F856 /* FeedCacheModel.swift */, 377F9F7E2944175F0043F856 /* FeedCacheModel.swift */,
3776925129463C310055EC18 /* PlaylistsCacheModel.swift */, 3776925129463C310055EC18 /* PlaylistsCacheModel.swift */,
37E64DD026D597EB00C71877 /* SubscribedChannelsModel.swift */, 37E64DD026D597EB00C71877 /* SubscribedChannelsModel.swift */,
@ -3052,6 +3057,7 @@
376A33E02720CAD6000C1D6B /* VideosApp.swift in Sources */, 376A33E02720CAD6000C1D6B /* VideosApp.swift in Sources */,
374AB3DB28BCAF7E00DF56FB /* SeekType.swift in Sources */, 374AB3DB28BCAF7E00DF56FB /* SeekType.swift in Sources */,
37192D5728B179D60012EEDD /* ChaptersView.swift in Sources */, 37192D5728B179D60012EEDD /* ChaptersView.swift in Sources */,
37D836BC294927E700005E5E /* ChannelsCacheModel.swift in Sources */,
37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */, 37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */,
37C0698227260B2100F7F6CB /* ThumbnailsModel.swift in Sources */, 37C0698227260B2100F7F6CB /* ThumbnailsModel.swift in Sources */,
37BC50A82778A84700510953 /* HistorySettings.swift in Sources */, 37BC50A82778A84700510953 /* HistorySettings.swift in Sources */,
@ -3254,6 +3260,7 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
37D836BD294927E700005E5E /* ChannelsCacheModel.swift in Sources */,
3727B74B27872B880021C15E /* VisualEffectBlur-macOS.swift in Sources */, 3727B74B27872B880021C15E /* VisualEffectBlur-macOS.swift in Sources */,
374710062755291C00CE0F87 /* SearchTextField.swift in Sources */, 374710062755291C00CE0F87 /* SearchTextField.swift in Sources */,
37F0F4EB286F397E00C06C2E /* SettingsModel.swift in Sources */, 37F0F4EB286F397E00C06C2E /* SettingsModel.swift in Sources */,
@ -3753,6 +3760,7 @@
37141675267A8E10006CA35D /* Country.swift in Sources */, 37141675267A8E10006CA35D /* Country.swift in Sources */,
370F500C27CC1821001B35DC /* MPVViewController.swift in Sources */, 370F500C27CC1821001B35DC /* MPVViewController.swift in Sources */,
3782B9542755667600990149 /* String+Format.swift in Sources */, 3782B9542755667600990149 /* String+Format.swift in Sources */,
37D836BE294927E700005E5E /* ChannelsCacheModel.swift in Sources */,
37152EEC26EFEB95004FB96D /* LazyView.swift in Sources */, 37152EEC26EFEB95004FB96D /* LazyView.swift in Sources */,
37EF9A78275BEB8E0043B585 /* CommentView.swift in Sources */, 37EF9A78275BEB8E0043B585 /* CommentView.swift in Sources */,
37484C2726FC83E000287258 /* InstanceForm.swift in Sources */, 37484C2726FC83E000287258 /* InstanceForm.swift in Sources */,