Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Playback 2024: Ratings #2376

Merged
merged 10 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,17 @@ class EndOfYearDataManager {

return EpisodesStartedAndCompleted(started: started, completed: completed)
}

func summarizedRatings(in year: Int) -> [UInt32: Int]? {
let calendar = Calendar.current
let ratings = DataManager.sharedManager.ratings.ratings?.filter { rating in
calendar.component(.year, from: rating.modifiedAt) == year
}
let groupedRatings = ratings?.reduce(into: [:]) { counts, rating in
counts[rating.podcastRating, default: 0] += 1
}
return groupedRatings
}
}

public struct ListenedCategory {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public class DataManager {

public let autoAddCandidates: AutoAddCandidatesDataManager
public let bookmarks: BookmarkDataManager
public let ratings: RatingsDataManager

private let dbQueue: FMDatabaseQueue

Expand Down Expand Up @@ -60,6 +61,7 @@ public class DataManager {

autoAddCandidates = AutoAddCandidatesDataManager(dbQueue: dbQueue)
bookmarks = BookmarkDataManager(dbQueue: dbQueue)
ratings = RatingsDataManager()
}

convenience init(endOfYearManager: EndOfYearDataManager) {
Expand Down Expand Up @@ -1133,5 +1135,10 @@ public extension DataManager {

func episodesStartedAndCompleted(in year: Int) -> EpisodesStartedAndCompleted {
endOfYearManager.episodesStartedAndCompleted(in: year, dbQueue: dbQueue)

}

func summarizedRatings(in year: Int) -> [UInt32: Int]? {
endOfYearManager.summarizedRatings(in: year)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Foundation

public struct UserPodcastRating: Codable {
public let podcastRating: UInt32
public let podcastUuid: String
public let modifiedAt: Date

public init(podcastRating: UInt32, podcastUuid: String, modifiedAt: Date) {
self.podcastRating = podcastRating
self.podcastUuid = podcastUuid
self.modifiedAt = modifiedAt
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/// This is currently in memory only since we don't store these locally in the database
public class RatingsDataManager {
public var ratings: [UserPodcastRating]?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import Foundation
import PocketCastsDataModel
import PocketCastsUtils
import SwiftProtobuf

class RetrieveRatingsTask: ApiBaseTask, @unchecked Sendable {
var completion: (([UserPodcastRating]?) -> Void)?

var success: Bool = false

private var convertedRatings = [UserPodcastRating]()

private lazy var addRatingGroup: DispatchGroup = {
let dispatchGroup = DispatchGroup()

return dispatchGroup
}()

override func apiTokenAcquired(token: String) {
let url = ServerConstants.Urls.api() + "user/podcast_rating/list"

do {
let (response, httpStatus) = getToServer(url: url, token: token)

guard let responseData = response, httpStatus?.statusCode == ServerConstants.HttpConstants.ok else {
completion?(nil)
return
}

let serverRatings = try Api_PodcastRatingsResponse(serializedBytes: responseData).podcastRatings
if serverRatings.count == 0 {
completion?(convertedRatings)

return
}

convertedRatings = serverRatings.map { rating in
UserPodcastRating(podcastRating: rating.podcastRating, podcastUuid: rating.podcastUuid, modifiedAt: rating.modifiedAt.date)
}

DataManager.sharedManager.ratings.ratings = convertedRatings

success = true

completion?(convertedRatings)
} catch {
FileLog.shared.addMessage("Decoding ratings failed \(error.localizedDescription)")
completion?(nil)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Foundation
import PocketCastsUtils
import SwiftProtobuf
import PocketCastsDataModel

class UserPodcastRatingAddTask: ApiBaseTask {
var completion: ((Bool) -> Void)?
Expand Down Expand Up @@ -88,9 +89,3 @@ class UserPodcastRatingGetTask: ApiBaseTask {
}
}
}

public struct UserPodcastRating: Codable {
public let podcastRating: UInt32
public let podcastUuid: String
public let modifiedAt: Date
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Foundation
import PocketCastsDataModel

public extension ApiServerHandler {
func addRating(uuid: String, rating: Int) async -> Bool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ class PodcastExistsHelper {
public class YearListeningHistory {
public static func sync() -> Bool {
var syncResults: [Bool] = []
let yearsToSync: [Int32] = SubscriptionHelper.hasActiveSubscription() ? [2023, 2022] : [2023]
let yearsToSync: [Int32] = SubscriptionHelper.hasActiveSubscription() ? [2024, 2023, 2022] : [2024, 2023]

let dispatchGroup = DispatchGroup()
yearsToSync.forEach { yearToSync in
Expand All @@ -205,6 +205,12 @@ public class YearListeningHistory {

syncResults.append(syncYearListeningHistory.success)

let syncRatings = RetrieveRatingsTask()

syncRatings.start()

syncResults.append(syncRatings.success)

dispatchGroup.leave()
}
}
Expand Down
8 changes: 8 additions & 0 deletions podcasts.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1667,6 +1667,7 @@
F52B4F8E2BB4A9ED00E87BE4 /* CategoriesSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F52B4F8D2BB4A9ED00E87BE4 /* CategoriesSelectorView.swift */; };
F533F19D2C24B8CB00EDE9AA /* ShareDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = F533F19C2C24B8CB00EDE9AA /* ShareDestination.swift */; };
F53C3E832CC73549004F3581 /* TopSpotStory2024.swift in Sources */ = {isa = PBXBuildFile; fileRef = F53C3E822CC73538004F3581 /* TopSpotStory2024.swift */; };
F53EFEEA2CD42AE400F4561B /* Ratings2024Story.swift in Sources */ = {isa = PBXBuildFile; fileRef = F53EFEE92CD42ADF00F4561B /* Ratings2024Story.swift */; };
F53EFEE82CD405DA00F4561B /* YearOverYearCompareTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F53EFEE72CD405DA00F4561B /* YearOverYearCompareTests.swift */; };
F543F6A42C0804FA00FEC8B6 /* AnyPublisher+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = F543F6A32C0804FA00FEC8B6 /* AnyPublisher+Async.swift */; };
F54E0F542CC764C0006D4DA2 /* PositionModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F54E0F532CC764C0006D4DA2 /* PositionModifier.swift */; };
Expand Down Expand Up @@ -1714,6 +1715,7 @@
F5D3A0D92B70950100EED067 /* MockURLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5D3A0D82B70950100EED067 /* MockURLHandler.swift */; };
F5D527382CC81AAD00682CD5 /* EpilogueStory2024.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5D527372CC81AA700682CD5 /* EpilogueStory2024.swift */; };
F5DBA58A2B756A8700AED77F /* PodcastSettings+ImportUserDefaultsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5DBA5892B756A8700AED77F /* PodcastSettings+ImportUserDefaultsTests.swift */; };
F5E16FE72CD47DE800C0372F /* SFSafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E16FE62CD47DE800C0372F /* SFSafariView.swift */; };
F5E3DAA72BDD5567002BD4E4 /* PCBundleDoc.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E3DAA62BDD5567002BD4E4 /* PCBundleDoc.swift */; };
F5E431D62B50888500A71DB3 /* PlusLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E431D52B50888500A71DB3 /* PlusLabel.swift */; };
F5E949DA2B61762E002DAFC3 /* TokenHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E949D92B61762E002DAFC3 /* TokenHelperTests.swift */; };
Expand Down Expand Up @@ -3616,6 +3618,7 @@
F52B4F8D2BB4A9ED00E87BE4 /* CategoriesSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoriesSelectorView.swift; sourceTree = "<group>"; };
F533F19C2C24B8CB00EDE9AA /* ShareDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareDestination.swift; sourceTree = "<group>"; };
F53C3E822CC73538004F3581 /* TopSpotStory2024.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopSpotStory2024.swift; sourceTree = "<group>"; };
F53EFEE92CD42ADF00F4561B /* Ratings2024Story.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ratings2024Story.swift; sourceTree = "<group>"; };
F53EFEE72CD405DA00F4561B /* YearOverYearCompareTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YearOverYearCompareTests.swift; sourceTree = "<group>"; };
F543F6A32C0804FA00FEC8B6 /* AnyPublisher+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AnyPublisher+Async.swift"; sourceTree = "<group>"; };
F54E0F532CC764C0006D4DA2 /* PositionModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionModifier.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3660,6 +3663,7 @@
F5D3A0D82B70950100EED067 /* MockURLHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockURLHandler.swift; sourceTree = "<group>"; };
F5D527372CC81AA700682CD5 /* EpilogueStory2024.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpilogueStory2024.swift; sourceTree = "<group>"; };
F5DBA5892B756A8700AED77F /* PodcastSettings+ImportUserDefaultsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PodcastSettings+ImportUserDefaultsTests.swift"; sourceTree = "<group>"; };
F5E16FE62CD47DE800C0372F /* SFSafariView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SFSafariView.swift; sourceTree = "<group>"; };
F5E3DAA62BDD5567002BD4E4 /* PCBundleDoc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PCBundleDoc.swift; sourceTree = "<group>"; };
F5E431D52B50888500A71DB3 /* PlusLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlusLabel.swift; sourceTree = "<group>"; };
F5E949D92B61762E002DAFC3 /* TokenHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenHelperTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -6973,6 +6977,7 @@
C74887782919DC7B00EBC926 /* SwiftUI */ = {
isa = PBXGroup;
children = (
F5E16FE62CD47DE800C0372F /* SFSafariView.swift */,
F54E0F532CC764C0006D4DA2 /* PositionModifier.swift */,
C7B3C60B2919DCC800054145 /* ActionView.swift */,
C7A110F8291F66C700887A90 /* Confetti.swift */,
Expand Down Expand Up @@ -7896,6 +7901,7 @@
F581ED412CC6F19300A19860 /* ListeningTime2024Story.swift */,
F5D527372CC81AA700682CD5 /* EpilogueStory2024.swift */,
F5F884642CCA86AA002BED2C /* LongestEpisode2024Story.swift */,
F53EFEE92CD42ADF00F4561B /* Ratings2024Story.swift */,
F581ED432CC6F3A400A19860 /* Top5Podcasts2024Story.swift */,
F5F884662CCA8F1F002BED2C /* CompletionRate2024Story.swift */,
F52987E82CD2ED7000628016 /* YearOverYearCompare2024Story.swift */,
Expand Down Expand Up @@ -9561,6 +9567,7 @@
F54E721D2CA7359800CD5C86 /* DiscoverCellType.swift in Sources */,
F51D90742C71A0A100F0A232 /* SharingFooterView.swift in Sources */,
BD240C43231F46F400FB2CDD /* PCSearchBarController+TextFieldDelegate.swift in Sources */,
F5E16FE72CD47DE800C0372F /* SFSafariView.swift in Sources */,
4053CDF9214F3583001C92B1 /* PodcastFilterOverlayController.swift in Sources */,
BDF5FD1E1FD8D38900F2A339 /* DownloadsViewController+Table.swift in Sources */,
C72CED32289DA1900017883A /* TracksAdapter.swift in Sources */,
Expand All @@ -9575,6 +9582,7 @@
C713D4FA2A04C90500A78468 /* AccountHeaderViewModel.swift in Sources */,
10756F882C59449C0089D34F /* Folder+Sortable.swift in Sources */,
F55C4C762BCF064500A10352 /* DiscoverViewController+CategoryRedesign.swift in Sources */,
F53EFEEA2CD42AE400F4561B /* Ratings2024Story.swift in Sources */,
F5954D602C3F2864004A8C04 /* TrimHandle.swift in Sources */,
BDF09F041E669E16009E9845 /* BasePlayPauseButton.swift in Sources */,
407B21E72231F6A300B4E492 /* UploadedSettingsViewController.swift in Sources */,
Expand Down
1 change: 1 addition & 0 deletions podcasts/Analytics/AnalyticsEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,7 @@ enum AnalyticsEvent: String {
case endOfYearStoryShared
case endOfYearProfileCardTapped
case endOfYearUpsellShown
case endOfYearLearnRatingsShown

// MARK: - Welcome View

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ class EndOfYear2023StoriesModel: StoryModel {
}
}

func hasLoadedData(in dataManager: DataManager) -> Bool {
true // Default data load of episodes is enough
}

func isReady() -> Bool {
if !stories.isEmpty {
stories.append(.intro)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ class EndOfYear2024StoriesModel: StoryModel {
data.listeningTime = listeningTime
}

// Ratings
if let ratings = dataManager.summarizedRatings(in: Self.year) {
data.ratings = ratings
stories.append(.ratings)
}

// Longest episode
if let longestEpisode = dataManager.longestEpisode(in: Self.year),
let podcast = longestEpisode.parentPodcast() {
Expand Down Expand Up @@ -75,6 +81,8 @@ class EndOfYear2024StoriesModel: StoryModel {
return TopSpotStory2024(topPodcast: data.topPodcasts.first!)
case .top5Podcasts:
return Top5Podcasts2024Story(top5Podcasts: data.topPodcasts)
case .ratings:
return Ratings2024Story(ratings: data.ratings)
case .listeningTime:
return ListeningTime2024Story(listeningTime: data.listeningTime)
case .longestEpisode:
Expand All @@ -94,11 +102,17 @@ class EndOfYear2024StoriesModel: StoryModel {
return true
case .top5Podcasts:
return true
case .ratings:
return true
default:
return false
}
}

func hasLoadedData(in dataManager: DataManager) -> Bool {
dataManager.ratings.ratings == nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit confusing. If I call hasLoadedData I'm not expecting ratings to be nil. Can we find a better name? If it's only related to ratings I would specify that in the function name.


func isReady() -> Bool {
if stories.isEmpty {
return false
Expand Down Expand Up @@ -156,4 +170,6 @@ class EndOfYear2024StoriesData {
var episodesStartedAndCompleted: EpisodesStartedAndCompleted!

var yearOverYearListeningTime: YearOverYearListeningTime!

var ratings: [UInt32: Int] = [:]
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ enum EndOfYear2024Story: CaseIterable {
case numberOfPodcastsAndEpisodesListened
case topSpot
case top5Podcasts
case ratings
case listeningTime
case longestEpisode
case yearOverYearListeningTime
Expand Down
5 changes: 4 additions & 1 deletion podcasts/End of Year/EndOfYearStoriesBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ class EndOfYearStoriesBuilder {
let modelType = type(of: model)

// Check if the user has the full listening history for this year
if SyncManager.isUserLoggedIn(), !Settings.hasSyncedEpisodesForPlayback(year: modelType.year) || (Settings.hasSyncedEpisodesForPlayback(year: modelType.year) && Settings.hasSyncedEpisodesForPlaybackAsPlusUser(year: modelType.year) != hasActiveSubscription()) {
if SyncManager.isUserLoggedIn(),
!Settings.hasSyncedEpisodesForPlayback(year: modelType.year) ||
(Settings.hasSyncedEpisodesForPlayback(year: modelType.year) && Settings.hasSyncedEpisodesForPlaybackAsPlusUser(year: modelType.year) != hasActiveSubscription()) || model.hasLoadedData(in: dataManager) {
let syncedWithSuccess = sync?()

if syncedWithSuccess == true {
Expand All @@ -53,6 +55,7 @@ protocol StoryModel {
func populate(with dataManager: DataManager)
func story(for storyNumber: Int) -> any StoryView
func isInteractiveView(for storyNumber: Int) -> Bool
func hasLoadedData(in dataManager: DataManager) -> Bool
func isReady() -> Bool
func paywallView() -> AnyView
/// Overlaid on top of the story
Expand Down
Loading