class Media(object): '''all media (mixed) widgets provided by the script''' def __init__(self, addon, artutils, options): '''Initializations pass our common classes and the widget options as arguments''' self.artutils = artutils self.addon = addon self.options = options self.movies = Movies(self.addon, self.artutils, self.options) self.tvshows = Tvshows(self.addon, self.artutils, self.options) self.songs = Songs(self.addon, self.artutils, self.options) self.albums = Albums(self.addon, self.artutils, self.options) self.pvr = Pvr(self.addon, self.artutils, self.options) self.episodes = Episodes(self.addon, self.artutils, self.options) def listing(self): '''main listing with all our movie nodes''' all_items = [ (self.addon.getLocalizedString(32011), "inprogress&mediatype=media", "DefaultMovies.png"), (self.addon.getLocalizedString(32005), "recent&mediatype=media", "DefaultMovies.png"), (self.addon.getLocalizedString(32004), "recommended&mediatype=media", "DefaultMovies.png"), (self.addon.getLocalizedString(32007), "inprogressandrecommended&mediatype=media", "DefaultMovies.png"), (self.addon.getLocalizedString(32060), "inprogressandrandom&mediatype=media", "DefaultMovies.png"), (self.addon.getLocalizedString(32022), "similar&mediatype=media", "DefaultMovies.png"), (self.addon.getLocalizedString(32059), "random&mediatype=media", "DefaultMovies.png"), (self.addon.getLocalizedString(32058), "top250&mediatype=media", "DefaultMovies.png"), (self.addon.getLocalizedString(32001), "favourites&mediatype=media", "DefaultMovies.png") ] return process_method_on_list(create_main_entry, all_items) def recommended(self): ''' get recommended media ''' all_items = self.movies.recommended() all_items += self.tvshows.recommended() all_items += self.albums.recommended() all_items += self.songs.recommended() all_items += self.episodes.recommended() return sorted(all_items, key=lambda k: random.random())[:self.options["limit"]] def recent(self): ''' get recently added media ''' all_items = self.movies.recent() all_items += self.albums.recent() all_items += self.songs.recent() all_items += self.episodes.recent() all_items += self.pvr.recordings() return sorted(all_items, key=itemgetter("dateadded"), reverse=True)[:self.options["limit"]] def random(self): ''' get random media ''' all_items = self.movies.random() all_items += self.tvshows.random() all_items += self.albums.random() all_items += self.songs.random() all_items += self.episodes.random() all_items += self.pvr.recordings() return sorted(all_items, key=lambda k: random.random())[:self.options["limit"]] def inprogress(self): ''' get in progress media ''' all_items = self.movies.inprogress() all_items += self.episodes.inprogress() all_items += self.pvr.recordings() return sorted(all_items, key=itemgetter("lastplayed"), reverse=True)[:self.options["limit"]] def similar(self): ''' get similar movies and similar tvshows for given imdbid''' all_items = self.movies.similar() all_items += self.tvshows.similar() all_items += self.albums.similar() all_items += self.songs.similar() return sorted(all_items, key=lambda k: random.random())[:self.options["limit"]] def inprogressandrecommended(self): ''' get recommended AND in progress media ''' all_items = self.inprogress() all_titles = [item["title"] for item in all_items] for item in self.recommended(): if item["title"] not in all_titles: all_items.append(item) return all_items[:self.options["limit"]] def inprogressandrandom(self): ''' get in progress AND random movies ''' all_items = self.inprogress() all_ids = [item["movieid"] for item in all_items] for item in self.random(): if item["movieid"] not in all_ids: all_items.append(item) return all_items[:self.options["limit"]] def top250(self): ''' get imdb top250 movies in library ''' all_items = self.movies.top250() all_items += self.tvshows.top250() return sorted(all_items, key=itemgetter("top250_rank"))[:self.options["limit"]] def favourites(self): '''get favourite media''' from favourites import Favourites self.options["mediafilter"] = "media" return Favourites(self.addon, self.artutils, self.options).favourites() def favourite(self): '''synonym to favourites''' return self.favourites()
class Media(object): '''all media (mixed) widgets provided by the script''' def __init__(self, addon, metadatautils, options): '''Initializations pass our common classes and the widget options as arguments''' self.metadatautils = metadatautils self.addon = addon self.options = options self.movies = Movies(self.addon, self.metadatautils, self.options) self.tvshows = Tvshows(self.addon, self.metadatautils, self.options) self.songs = Songs(self.addon, self.metadatautils, self.options) self.albums = Albums(self.addon, self.metadatautils, self.options) self.pvr = Pvr(self.addon, self.metadatautils, self.options) self.episodes = Episodes(self.addon, self.metadatautils, self.options) def listing(self): '''main listing with all our movie nodes''' all_items = [ (self.addon.getLocalizedString(32011), "inprogress&mediatype=media", "DefaultMovies.png"), (self.addon.getLocalizedString(32070), "inprogressshowsandmovies&mediatype=media", "DefaultMovies.png"), (self.addon.getLocalizedString(32005), "recent&mediatype=media", "DefaultMovies.png"), (self.addon.getLocalizedString(32004), "recommended&mediatype=media", "DefaultMovies.png"), (self.addon.getLocalizedString(32007), "inprogressandrecommended&mediatype=media", "DefaultMovies.png"), (self.addon.getLocalizedString(32060), "inprogressandrandom&mediatype=media", "DefaultMovies.png"), (self.addon.getLocalizedString(32022), "similar&mediatype=media", "DefaultMovies.png"), (self.addon.getLocalizedString(32059), "random&mediatype=media", "DefaultMovies.png"), (self.addon.getLocalizedString(32058), "top250&mediatype=media", "DefaultMovies.png"), (self.addon.getLocalizedString(32001), "favourites&mediatype=media", "DefaultMovies.png"), (self.addon.getLocalizedString(32075), "playlistslisting&mediatype=media", "DefaultMovies.png"), (self.addon.getLocalizedString(32077), "playlistslisting&mediatype=media&tag=ref", "DefaultMovies.png") ] return self.metadatautils.process_method_on_list(create_main_entry, all_items) def playlistslisting(self): '''get tv playlists listing''' #TODO: append (Movie playlist) and (TV Show Playlist) #TODO: only show playlists with appropriate type (Movie or TV Show) movie_label = self.options.get("movie_label") tag_label = self.options.get("tag") all_items = [] for item in self.metadatautils.kodidb.files("special://videoplaylists/"): # replace '&' with [and] -- will get fixed when processed in playlist action label = item["label"].replace('&', '[and]') if tag_label == 'ref': if movie_label: details = (item["label"], "refplaylist&mediatype=media&movie_label=%s&tv_label=%s" % (movie_label, label), "DefaultTvShows.png") else: details = (item["label"], "playlistslisting&mediatype=media&tag=ref&movie_label=%s" % label, "DefaultMovies.png") else: if movie_label: details = (item["label"], "playlist&mediatype=media&movie_label=%s&tv_label=%s" % (movie_label, label), "DefaultTvShows.png") else: details = (item["label"], "playlistslisting&mediatype=media&movie_label=%s" % label, "DefaultMovies.png") all_items.append(create_main_entry(details)) return all_items def playlist(self): '''get items in both playlists, sorted by recommended score''' movie_label = self.options.get("movie_label").replace('[and]', '&') tv_label = self.options.get("tv_label").replace('[and]', '&') movies = self.metadatautils.kodidb.movies( filters=[{"operator": "is", "field": "playlist", "value": movie_label}]) tvshows = self.metadatautils.kodidb.tvshows( filters=[{"operator": "is", "field": "playlist", "value": tv_label}]) tvshows = self.metadatautils.process_method_on_list(self.tvshows.process_tvshow, tvshows) all_items = self.sort_by_recommended(movies+tvshows) return sorted(all_items, key=itemgetter("recommendedscore"), reverse=True)[:self.options["limit"]] def refplaylist(self): '''get items similar to items in playlists ''' movie_label = self.options.get("movie_label").replace('[and]', '&') tv_label = self.options.get("tv_label").replace('[and]', '&') ref_movies = self.metadatautils.kodidb.movies( filters=[{"operator": "is", "field": "playlist", "value": movie_label}]) ref_tvshows = self.metadatautils.kodidb.tvshows( filters=[{"operator": "is", "field": "playlist", "value": tv_label}]) movies = self.metadatautils.kodidb.movies( filters=[{"operator": "isnot", "field": "playlist", "value": movie_label}]) tvshows = self.metadatautils.kodidb.tvshows( filters=[{"operator": "isnot", "field": "playlist", "value": tv_label}]) tvshows = self.metadatautils.process_method_on_list(self.tvshows.process_tvshow, tvshows) all_items = self.sort_by_recommended(movies+tvshows, ref_movies+ref_tvshows) return sorted(all_items, key=itemgetter("recommendedscore"), reverse=True)[:self.options["limit"]] def recommended(self): ''' get recommended media ''' if self.options["exp_recommended"]: # get all unwatched, not in-progess movies & tvshows movies = self.metadatautils.kodidb.movies(filters=[kodi_constants.FILTER_UNWATCHED]) tvshows = self.metadatautils.kodidb.tvshows(filters=[kodi_constants.FILTER_UNWATCHED]) tvshows = self.metadatautils.process_method_on_list(self.tvshows.process_tvshow, tvshows) # return list sorted by recommended score, and capped by limit return self.sort_by_recommended(movies+tvshows) all_items = self.movies.recommended() all_items += self.tvshows.recommended() all_items += self.albums.recommended() all_items += self.songs.recommended() all_items += self.episodes.recommended() return sorted(all_items, key=lambda k: random.random())[:self.options["limit"]] def recent(self): ''' get recently added media ''' all_items = self.movies.recent() all_items += self.albums.recent() all_items += self.songs.recent() all_items += self.episodes.recent() all_items += self.pvr.recordings() return sorted(all_items, key=itemgetter("dateadded"), reverse=True)[:self.options["limit"]] def random(self): ''' get random media ''' all_items = self.movies.random() all_items += self.tvshows.random() all_items += self.albums.random() all_items += self.songs.random() all_items += self.episodes.random() all_items += self.pvr.recordings() return sorted(all_items, key=lambda k: random.random())[:self.options["limit"]] def inprogress(self): ''' get in progress media ''' all_items = self.movies.inprogress() all_items += self.episodes.inprogress() all_items += self.pvr.recordings() return sorted(all_items, key=itemgetter("lastplayed"), reverse=True)[:self.options["limit"]] def inprogressshowsandmovies(self): ''' get in progress media ''' all_items = self.movies.inprogress() all_items += self.episodes.inprogress() return sorted(all_items, key=itemgetter("lastplayed"), reverse=True)[:self.options["limit"]] def similar(self): ''' get similar movies and similar tvshows for given imdbid''' if self.options["exp_recommended"]: # get ref item, and check if movie ref_item = self.get_recently_watched_item() is_ref_movie = ref_item.has_key("uniqueid") # create list of all items if self.options["hide_watched_similar"]: all_items = self.metadatautils.kodidb.movies(filters=[kodi_constants.FILTER_UNWATCHED]) all_items += self.metadatautils.process_method_on_list( self.tvshows.process_tvshow, self.metadatautils.kodidb.tvshows( filters=[kodi_constants.FILTER_UNWATCHED])) else: all_items = self.metadatautils.kodidb.movies() all_items += self.metadatautils.process_method_on_list( self.tvshows.process_tvshow, self.metadatautils.kodidb.tvshows()) if not ref_item: return None if is_ref_movie: # define sets for speed set_genres = set(ref_item["genre"]) set_directors = set(ref_item["director"]) set_writers = set(ref_item["writer"]) set_cast = set([x["name"] for x in ref_item["cast"][:5]]) sets = (set_genres, set_directors, set_writers, set_cast) # get similarity score for all items for item in all_items: if item.has_key("uniqueid"): # if item is also movie, check if it's the ref_item if item["title"] == ref_item["title"] and item["year"] == ref_item["year"]: # don't rank the reference movie similarscore = 0 else: # otherwise, use movie method for score similarscore = self.movies.get_similarity_score( ref_item, item, sets=sets) else: # if item isn't movie, use mixed method similarscore = self.get_similarity_score(ref_item, item) # set extraproperties item["similarscore"] = similarscore item["extraproperties"] = {"similartitle": ref_item["title"], "originalpath": item["file"]} else: # define sets for speed set_genres = set(ref_item["genre"]) set_cast = set([x["name"] for x in ref_item["cast"][:10]]) sets = (set_genres, set_cast) # get similarity score for all items for item in all_items: if not item.has_key("uniqueid"): # if item is also tvshow, check if it's the ref_item if item["title"] == ref_item["title"] and item["year"] == ref_item["year"]: # don't rank the reference movie similarscore = 0 else: # otherwise, use tvshow method for score similarscore = self.tvshows.get_similarity_score( ref_item, item, sets=sets) else: # if item isn't tvshow, use mixed method similarscore = self.get_similarity_score(ref_item, item) # set extraproperties item["similarscore"] = similarscore item["extraproperties"] = {"similartitle": ref_item["title"], "originalpath": item["file"]} # return list sorted by score and capped by limit return sorted(all_items, key=itemgetter("similarscore"), reverse=True)[:self.options["limit"]] all_items = self.movies.similar() all_items += self.tvshows.similar() all_items += self.albums.similar() all_items += self.songs.similar() return sorted(all_items, key=lambda k: random.random())[:self.options["limit"]] def inprogressandrecommended(self): ''' get recommended AND in progress media ''' all_items = self.inprogress() all_titles = [item["title"] for item in all_items] for item in self.recommended(): if item["title"] not in all_titles: all_items.append(item) return all_items[:self.options["limit"]] def inprogressandrandom(self): ''' get in progress AND random movies ''' all_items = self.inprogress() all_ids = [item["movieid"] for item in all_items] for item in self.random(): if item["movieid"] not in all_ids: all_items.append(item) return all_items[:self.options["limit"]] def top250(self): ''' get imdb top250 movies in library ''' all_items = self.movies.top250() all_items += self.tvshows.top250() return sorted(all_items, key=itemgetter("top250_rank"))[:self.options["limit"]] def favourites(self): '''get favourite media''' from favourites import Favourites self.options["mediafilter"] = "media" return Favourites(self.addon, self.metadatautils, self.options).favourites() def favourite(self): '''synonym to favourites''' return self.favourites() def get_recently_watched_item(self): ''' get a random recently watched movie or tvshow ''' num_recent_similar = self.options["num_recent_similar"] # get recently played movies recent_items = self.metadatautils.kodidb.movies(sort=kodi_constants.SORT_LASTPLAYED, filters=[kodi_constants.FILTER_WATCHED], limits=(0, num_recent_similar)) # get recently played episodes recent_items += self.metadatautils.kodidb.episodes(sort=kodi_constants.SORT_LASTPLAYED, filters=[kodi_constants.FILTER_WATCHED], limits=(0, num_recent_similar)) # sort all by last played, then randomly pick recent_items = sorted(recent_items, key=itemgetter("lastplayed"), reverse=True)[:num_recent_similar] item = random.choice(recent_items) # if item is an episode, get its tvshow if not item.has_key("genre"): show_title = item['showtitle'] title_filter = [{"field": "title", "operator": "is", "value": "%s" % show_title}] tvshow = self.metadatautils.kodidb.tvshows(filters=title_filter, limits=(0, 1)) return tvshow[0] return item def sort_by_recommended(self, all_items, ref_items=None): ''' sort list of mixed movies/tvshows by recommended score''' # use recent items if ref_items not given if not ref_items: num_recent_similar = self.options["num_recent_similar"] # get recently watched movies movies = self.metadatautils.kodidb.movies(sort=kodi_constants.SORT_LASTPLAYED, filters=[kodi_constants.FILTER_WATCHED], limits=(0, 2*num_recent_similar)) # get recently watched episodes episodes = self.metadatautils.kodidb.episodes(sort=kodi_constants.SORT_LASTPLAYED, filters=[kodi_constants.FILTER_WATCHED], limits=(0, 2*num_recent_similar)) # get tvshows from episodes tvshows = self.tvshows.get_tvshows_from_episodes(episodes) # combine lists and sort by last played recent items = sorted(movies + tvshows, key=itemgetter('lastplayed'), reverse=True) # find duplicates and set weights titles = set() ref_items = list() weights = dict() weight_sum = 0 for item in items: title = item['title'] if title in titles: weights[title] += 0.5 weight_sum += 0.5 else: ref_items.append(item) titles.add(title) weights[title] = 1 weight_sum += 1 if weight_sum >= num_recent_similar: break del titles, items, weight_sum else: # set equal weights for pre-defined ref_items weights = dict() for item in ref_items: weights[item['title']] = 1 # average scores together for every item for item in all_items: similarscore = 0 for ref_item in ref_items: title = ref_item['title'] # add all similarscores for item if ref_item.has_key("uniqueid") and item.has_key("uniqueid"): # use movies method if both items are movies similarscore += weights[title] * self.movies.get_similarity_score(ref_item, item) elif ref_item.has_key("uniqueid") or item.has_key("uniqueid"): # use media method if only one item is a movie similarscore += weights[title] * self.get_similarity_score(ref_item, item) else: # use tvshows method if neither items are movies similarscore += weights[title] * self.tvshows.get_similarity_score(ref_item, item) # average score and scale down based on playcount item["recommendedscore"] = similarscore / (1+item["playcount"]) / len(ref_items) # return sorted list capped by limit return sorted(all_items, key=itemgetter("recommendedscore"), reverse=True)[:self.options["limit"]] def get_similarity_score(self, ref_item, other_item): ''' get a similarity score (0-.625) between movie and tvshow ''' # get set of genres if ref_item.has_key("uniqueid"): set_genres = set(ref_item["genre"]) else: # change genres to movie equivalents if tvshow set_genres = self.convert_tvshow_genres(ref_item["genre"]) set_cast = set([x["name"] for x in ref_item["cast"][:5]]) # calculate individual scores for contributing factors # genre_score = (numer of matching genres) / (number of unique genres between both) genre_score = 0 if not set_genres else \ float(len(set_genres.intersection(other_item["genre"]))) / \ len(set_genres.union(other_item["genre"])) # cast_score is normalized by fixed amount of 5, and scaled up nonlinearly cast_score = (float(len(set_cast.intersection([x["name"] for x in other_item["cast"][:5]])))/5)**(1./2) # rating_score is "closeness" in rating, scaled to 1 if ref_item["rating"] and other_item["rating"] and abs(ref_item["rating"]-other_item["rating"]) < 3: rating_score = 1 - abs(ref_item["rating"]-other_item["rating"])/3 else: rating_score = 0 # year_score is "closeness" in release year, scaled to 1 (0 if not from same decade) if ref_item["year"] and other_item["year"] and abs(ref_item["year"]-other_item["year"]) < 10: year_score = 1 - abs(ref_item["year"]-other_item["year"])/10 else: year_score = 0 # calculate overall score using weighted average similarscore = .5*genre_score + .05*cast_score + .025*rating_score + .05*year_score return similarscore @staticmethod def convert_tvshow_genres(genres): ''' converts tvshow genres into movie genre equivalent ''' mapped_genres = {'TV Documentaries': 'Documentary', 'TV Sci-Fi & Fantasy': 'Sci-Fi & Fantasy', 'TV Action & Adventure': 'Action & Adventure', 'TV Comedies': 'Comedy', 'TV Mysteries': 'Mystery', 'TV Westerns': 'Westerns', 'TV Dramas': 'Drama', 'TV Crime Dramas': 'Crime Dramas', } for genre in genres: if mapped_genres.has_key(genre): genre = mapped_genres[genre] return set(genres)