|
8 | 8 | # noinspection PyPackages |
9 | 9 | from bossanova808.playback import Playback |
10 | 10 | # noinspection PyPackages |
11 | | -from bossanova808.utilities import get_resume_point, get_playcount, get_advancedsetting |
| 11 | +from bossanova808.utilities import get_playcount_and_resume_point, get_advancedsetting |
12 | 12 | # noinspection PyPackages |
13 | 13 | from bossanova808.logger import Logger |
14 | 14 |
|
@@ -69,47 +69,68 @@ def load_or_init(self) -> None: |
69 | 69 |
|
70 | 70 | list_needs_save = False |
71 | 71 |
|
72 | | - # If the user wants to filter out watched items from the list |
73 | | - if self.remove_watched_playbacks: |
74 | | - paths_to_remove = [] |
75 | | - for item in list(self.list): |
76 | | - # DB item? Is it marked as watched in the DB? |
77 | | - if item.dbid: |
78 | | - playcount = get_playcount(item.type, item.dbid) |
| 72 | + # Defensively collapse any duplicate entries for the same piece of media - there should only |
| 73 | + # ever be one entry (the most recent) per movie/episode/etc. Duplicates can end up here from |
| 74 | + # an older version of the addon that deduplicated on .path alone, which isn't always stable |
| 75 | + # for the same media across plays (see Playback.identity_key). The list is newest-first, so |
| 76 | + # keeping the first occurrence of each identity keeps the most recent one. |
| 77 | + seen_identities = set() |
| 78 | + deduplicated_list = [] |
| 79 | + for item in self.list: |
| 80 | + identity = item.identity_key |
| 81 | + if identity in seen_identities: |
| 82 | + Logger.warning(f"Removing duplicate PlaybackList entry for [{item.pluginlabel}] (identity: {identity})") |
| 83 | + list_needs_save = True |
| 84 | + continue |
| 85 | + seen_identities.add(identity) |
| 86 | + deduplicated_list.append(item) |
| 87 | + self.list = deduplicated_list |
| 88 | + |
| 89 | + # Refresh resume points from the Kodi library (consider e.g. shared library scenarios), and - |
| 90 | + # if the user wants it - filter out watched items, for every library item in one pass. Both |
| 91 | + # pieces of data come from the same JSON-RPC call per dbid, rather than two separate |
| 92 | + # round trips, since a list with several library items otherwise means twice as many |
| 93 | + # JSON-RPC calls on every single load (e.g. just viewing the Switchback list). |
| 94 | + paths_to_remove = [] |
| 95 | + for item in list(self.list): |
| 96 | + # DB item? Refresh its resume point, and its playcount if we care about that here |
| 97 | + if item.dbid: |
| 98 | + playcount, library_resume_point = get_playcount_and_resume_point(item.type, item.dbid) |
| 99 | + |
| 100 | + if self.remove_watched_playbacks: |
79 | 101 | if playcount is None: |
80 | 102 | Logger.warning(f"dbid {item.dbid} no longer valid in Kodi library for [{item.pluginlabel}] - removing from Switchback list") |
81 | 103 | paths_to_remove.append(item.path) |
82 | 104 | list_needs_save = True |
| 105 | + continue |
83 | 106 | elif playcount > 0: |
84 | 107 | list_needs_save = True |
85 | 108 | Logger.warning(f"Filtering watched playback from the list (as playcount > 0 in Kodi DB): [{item.pluginlabel}]") |
86 | 109 | paths_to_remove.append(item.path) |
| 110 | + continue |
87 | 111 |
|
88 | | - # Not a DB item, use a calculation instead and compare to the playcount_minium_percent |
89 | | - elif item.resumetime and item.totaltime: |
90 | | - percent_played = (item.resumetime / item.totaltime) * 100 |
91 | | - # Use the user set playcount_minium_percent if there is one, or fallback to Kodi default 90 percent |
92 | | - setting = get_advancedsetting('video/playcountminimumpercent') |
93 | | - playcount_minium_percent = float(setting) if setting and setting != 0 else 90.0 |
94 | | - if percent_played >= playcount_minium_percent: |
95 | | - list_needs_save = True |
96 | | - Logger.debug(f"Filtering watched playback from the list (as {percent_played:.1f}% played over playcount_minium_percent {playcount_minium_percent}%): [{item.pluginlabel}]") |
97 | | - paths_to_remove.append(item.path) |
98 | | - |
99 | | - if paths_to_remove: |
100 | | - list_needs_save = True |
101 | | - for path in paths_to_remove: |
102 | | - self.remove_playbacks_of_path(path) |
103 | | - |
104 | | - # Update resume points with current data from the Kodi library (consider e.g. shared library scenarios) |
105 | | - for item in self.list: |
106 | | - if item.dbid: |
107 | | - library_resume_point = get_resume_point(item.type, item.dbid) |
108 | 112 | if library_resume_point != item.resumetime: |
109 | 113 | Logger.debug(f"Retrieved library resume point: {library_resume_point} != existing list resume point {item.resumetime} - updating playback list") |
110 | 114 | list_needs_save = True |
111 | 115 | item.resumetime = library_resume_point |
112 | 116 |
|
| 117 | + # Not a DB item - if the user wants watched items filtered, use a calculation instead |
| 118 | + # and compare to the playcount_minium_percent (there's no library playcount to check) |
| 119 | + elif self.remove_watched_playbacks and item.resumetime and item.totaltime: |
| 120 | + percent_played = (item.resumetime / item.totaltime) * 100 |
| 121 | + # Use the user set playcount_minium_percent if there is one, or fallback to Kodi default 90 percent |
| 122 | + setting = get_advancedsetting('video/playcountminimumpercent') |
| 123 | + playcount_minium_percent = float(setting) if setting and setting != 0 else 90.0 |
| 124 | + if percent_played >= playcount_minium_percent: |
| 125 | + list_needs_save = True |
| 126 | + Logger.debug(f"Filtering watched playback from the list (as {percent_played:.1f}% played over playcount_minium_percent {playcount_minium_percent}%): [{item.pluginlabel}]") |
| 127 | + paths_to_remove.append(item.path) |
| 128 | + |
| 129 | + if paths_to_remove: |
| 130 | + list_needs_save = True |
| 131 | + for path in paths_to_remove: |
| 132 | + self.remove_playbacks_of_path(path) |
| 133 | + |
113 | 134 | if list_needs_save: |
114 | 135 | self.save_to_file() |
115 | 136 |
|
@@ -175,3 +196,24 @@ def find_playback_by_path(self, path: str) -> Optional[Playback]: |
175 | 196 | return playback |
176 | 197 | Logger.debug(f"No matching playback for [{path}]") |
177 | 198 | return None |
| 199 | + |
| 200 | + def find_playback_by_identity(self, playback: Playback) -> Optional[Playback]: |
| 201 | + """ |
| 202 | + Return an existing list entry representing the same piece of media as the given playback |
| 203 | + (see Playback.identity_key), if found, otherwise None. Unlike find_playback_by_path(), this |
| 204 | + still recognises the same library movie/episode/etc even if the specific path/file Kodi |
| 205 | + reports for it differs between plays (e.g. a direct library click vs an addon-triggered |
| 206 | + Switchback replay of the same item) - which is what actually determines whether a repeat |
| 207 | + play updates the existing entry or wrongly creates a duplicate. |
| 208 | +
|
| 209 | + :param playback: the Playback to find an existing match for |
| 210 | + :return: Playback or None: The matching Playback object if found, otherwise None |
| 211 | + """ |
| 212 | + identity = playback.identity_key |
| 213 | + Logger.debug(f"find_playback_by_identity: {identity}") |
| 214 | + for existing in self.list: |
| 215 | + if existing.identity_key == identity: |
| 216 | + Logger.debug(f"Matched playback to [{existing.pluginlabel}]") |
| 217 | + return existing |
| 218 | + Logger.debug(f"No matching playback for identity [{identity}]") |
| 219 | + return None |
0 commit comments