From 72b6fd62a0d0d2065f5d1038b07694ed726ac17e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louis=20V=C3=A9zina?= <5130500+morpheus65535@users.noreply.github.com> Date: Mon, 13 Jul 2020 10:57:43 -0400 Subject: [PATCH] Added filter to exclude episodes based on series type as defined in Sonarr. --- bazarr/api.py | 33 +- bazarr/config.py | 3 +- bazarr/database.py | 19 +- bazarr/get_series.py | 8 +- bazarr/get_subtitle.py | 29 +- static/css/tagsinput.css | 103 ++++++ static/js/tagsinput.js | 691 ++++++++++++++++++++++++++++++++++++++ views/episodes.html | 2 + views/settingssonarr.html | 25 +- 9 files changed, 876 insertions(+), 37 deletions(-) create mode 100644 static/css/tagsinput.css create mode 100644 static/js/tagsinput.js diff --git a/bazarr/api.py b/bazarr/api.py index b9050e82a..ef765ceef 100644 --- a/bazarr/api.py +++ b/bazarr/api.py @@ -78,9 +78,9 @@ class Restart(Resource): class Badges(Resource): @authenticate def get(self): - missing_episodes = database.execute("SELECT table_shows.tags, table_episodes.monitored FROM table_episodes " - "INNER JOIN table_shows on table_shows.sonarrSeriesId = " - "table_episodes.sonarrSeriesId WHERE missing_subtitles is not null AND " + missing_episodes = database.execute("SELECT table_shows.tags, table_episodes.monitored, table_shows.seriesType " + "FROM table_episodes INNER JOIN table_shows on table_shows.sonarrSeriesId =" + " table_episodes.sonarrSeriesId WHERE missing_subtitles is not null AND " "missing_subtitles != '[]'") missing_episodes = filter_exclusions(missing_episodes, 'series') missing_episodes = len(missing_episodes) @@ -293,6 +293,9 @@ class Series(Resource): # Parse tags item.update({"tags": ast.literal_eval(item['tags'])}) + # Parse seriesType + item.update({"seriesType": item['seriesType'].capitalize()}) + # Provide mapped path mapped_path = path_mappings.path_replace(item['path']) item.update({"mapped_path": mapped_path}) @@ -301,9 +304,9 @@ class Series(Resource): item.update({"exist": os.path.isdir(mapped_path)}) # Add missing subtitles episode count - episodeMissingCount = database.execute("SELECT table_shows.tags, table_episodes.monitored FROM " - "table_episodes INNER JOIN table_shows on " - "table_shows.sonarrSeriesId = table_episodes.sonarrSeriesId " + episodeMissingCount = database.execute("SELECT table_shows.tags, table_episodes.monitored, " + "table_shows.seriesType FROM table_episodes INNER JOIN table_shows " + "on table_shows.sonarrSeriesId = table_episodes.sonarrSeriesId " "WHERE table_episodes.sonarrSeriesId=? AND missing_subtitles is not " "null AND missing_subtitles != '[]'", (item['sonarrSeriesId'],)) episodeMissingCount = filter_exclusions(episodeMissingCount, 'series') @@ -311,10 +314,10 @@ class Series(Resource): item.update({"episodeMissingCount": episodeMissingCount}) # Add episode count - episodeFileCount = database.execute("SELECT table_shows.tags, table_episodes.monitored FROM table_episodes " - "INNER JOIN table_shows on table_shows.sonarrSeriesId = " - "table_episodes.sonarrSeriesId WHERE table_episodes.sonarrSeriesId=?", - (item['sonarrSeriesId'],)) + episodeFileCount = database.execute("SELECT table_shows.tags, table_episodes.monitored, " + "table_shows.seriesType FROM table_episodes INNER JOIN table_shows on " + "table_shows.sonarrSeriesId = table_episodes.sonarrSeriesId WHERE " + "table_episodes.sonarrSeriesId=?", (item['sonarrSeriesId'],)) episodeFileCount = filter_exclusions(episodeFileCount, 'series') episodeFileCount = len(episodeFileCount) item.update({"episodeFileCount": episodeFileCount}) @@ -1122,9 +1125,9 @@ class HistorySeries(Resource): query_actions = [1, 3] upgradable_episodes = database.execute( - "SELECT video_path, MAX(timestamp) as timestamp, score, table_shows.tags, table_episodes.monitored FROM " - "table_history INNER JOIN table_episodes on table_episodes.sonarrEpisodeId = " - "table_history.sonarrEpisodeId INNER JOIN table_shows on table_shows.sonarrSeriesId = " + "SELECT video_path, MAX(timestamp) as timestamp, score, table_shows.tags, table_episodes.monitored, " + "table_shows.seriesType FROM table_history INNER JOIN table_episodes on table_episodes.sonarrEpisodeId " + "= table_history.sonarrEpisodeId INNER JOIN table_shows on table_shows.sonarrSeriesId = " "table_episodes.sonarrSeriesId WHERE action IN (" + ','.join(map(str, query_actions)) + ") AND timestamp > ? AND " "score is not null GROUP BY table_history.video_path, table_history.language", @@ -1326,8 +1329,8 @@ class WantedSeries(Resource): "table_episodes.title as episodeTitle, table_episodes.missing_subtitles, " "table_episodes.sonarrSeriesId, table_episodes.path, table_shows.hearing_impaired, " "table_episodes.sonarrEpisodeId, table_episodes.scene_name, table_shows.tags, " - "table_episodes.failedAttempts FROM table_episodes INNER JOIN table_shows on " - "table_shows.sonarrSeriesId = table_episodes.sonarrSeriesId WHERE " + "table_episodes.failedAttempts, table_shows.seriesType FROM table_episodes INNER JOIN " + "table_shows on table_shows.sonarrSeriesId = table_episodes.sonarrSeriesId WHERE " "table_episodes.missing_subtitles != '[]' ORDER BY table_episodes._rowid_ DESC LIMIT ? " "OFFSET ?", (length, start)) data = filter_exclusions(data, 'series') diff --git a/bazarr/config.py b/bazarr/config.py index deea1d1db..bf325c9bd 100644 --- a/bazarr/config.py +++ b/bazarr/config.py @@ -76,7 +76,8 @@ defaults = { 'only_monitored': 'False', 'series_sync': '1', 'episodes_sync': '5', - 'excluded_tags': '[]' + 'excluded_tags': '[]', + 'excluded_series_types': '[]' }, 'radarr': { 'ip': '127.0.0.1', diff --git a/bazarr/database.py b/bazarr/database.py index 968d86351..0125d779f 100644 --- a/bazarr/database.py +++ b/bazarr/database.py @@ -93,6 +93,7 @@ def db_upgrade(): ['table_shows', 'alternateTitles', 'text'], ['table_shows', 'forced', 'text', 'False'], ['table_shows', 'tags', 'text', '[]'], + ['table_shows', 'seriesType', 'text', ''], ['table_episodes', 'format', 'text'], ['table_episodes', 'resolution', 'text'], ['table_episodes', 'video_codec', 'text'], @@ -144,9 +145,21 @@ def filter_exclusions(dicts_list, type): else: tagsList = ast.literal_eval(settings.radarr.excluded_tags) monitoredOnly = settings.radarr.getboolean('only_monitored') + + # Filter tags dictsList_tags_filtered = [item for item in dicts_list if set(tagsList).isdisjoint(ast.literal_eval(item['tags']))] + + # Filter monitored if monitoredOnly: - dictsList_tags_monitored = [item for item in dictsList_tags_filtered if item['monitored'] == 'True'] + dictsList_monitored_filtered = [item for item in dictsList_tags_filtered if item['monitored'] == 'True'] + else: + dictsList_monitored_filtered = dictsList_tags_filtered + + # Filter series type + if type == 'series': + dictsList_types_filtered = [item for item in dictsList_monitored_filtered if item['seriesType'] not in + ast.literal_eval(settings.sonarr.excluded_series_types)] else: - dictsList_tags_monitored = dictsList_tags_filtered - return dictsList_tags_monitored + dictsList_types_filtered = dictsList_monitored_filtered + + return dictsList_types_filtered diff --git a/bazarr/get_series.py b/bazarr/get_series.py index 29367346b..fb263ff8f 100644 --- a/bazarr/get_series.py +++ b/bazarr/get_series.py @@ -98,7 +98,8 @@ def update_series(): 'sortTitle': show['sortTitle'], 'year': str(show['year']), 'alternateTitles': alternate_titles, - 'tags': str(tags)}) + 'tags': str(tags), + 'seriesType': show['seriesType']}) else: series_to_add.append({'title': show["title"], 'path': show["path"], @@ -114,7 +115,8 @@ def update_series(): 'year': str(show['year']), 'alternateTitles': alternate_titles, 'forced': serie_default_forced, - 'tags': str(tags)}) + 'tags': str(tags), + 'seriesType': show['seriesType']}) # Remove old series from DB removed_series = list(set(current_shows_db_list) - set(current_shows_sonarr)) @@ -126,7 +128,7 @@ def update_series(): # Update existing series in DB series_in_db_list = [] series_in_db = database.execute("SELECT title, path, tvdbId, sonarrSeriesId, overview, poster, fanart, " - "audio_language, sortTitle, year, alternateTitles, tags FROM table_shows") + "audio_language, sortTitle, year, alternateTitles, tags, seriesType FROM table_shows") for item in series_in_db: series_in_db_list.append(item) diff --git a/bazarr/get_subtitle.py b/bazarr/get_subtitle.py index 9bbfcbeda..7d7112b89 100644 --- a/bazarr/get_subtitle.py +++ b/bazarr/get_subtitle.py @@ -631,10 +631,10 @@ def manual_upload_subtitle(path, language, forced, title, scene_name, media_type def series_download_subtitles(no): episodes_details = database.execute("SELECT table_episodes.path, table_episodes.missing_subtitles, monitored, " - "table_episodes.sonarrEpisodeId, table_episodes.scene_name, table_shows.tags " - "FROM table_episodes INNER JOIN table_shows on table_shows.sonarrSeriesId = " - "table_episodes.sonarrSeriesId WHERE table_episodes.sonarrSeriesId=? and " - "missing_subtitles!='[]'", (no,)) + "table_episodes.sonarrEpisodeId, table_episodes.scene_name, table_shows.tags, " + "table_shows.seriesType FROM table_episodes INNER JOIN table_shows on " + "table_shows.sonarrSeriesId = table_episodes.sonarrSeriesId WHERE " + "table_episodes.sonarrSeriesId=? and missing_subtitles!='[]'", (no,)) episodes_details = filter_exclusions(episodes_details, 'series') if not episodes_details: logging.debug("BAZARR no episode for that sonarrSeriesId can be found in database:", str(no)) @@ -685,9 +685,9 @@ def episode_download_subtitles(no): episodes_details = database.execute("SELECT table_episodes.path, table_episodes.missing_subtitles, monitored, " "table_episodes.sonarrEpisodeId, table_episodes.scene_name, table_shows.tags, " "table_shows.hearing_impaired, table_shows.title, table_shows.sonarrSeriesId, " - "table_shows.forced, table_shows.audio_language FROM table_episodes LEFT JOIN " - "table_shows on table_episodes.sonarrSeriesId = table_shows.sonarrSeriesId " - "WHERE sonarrEpisodeId=?", (no,)) + "table_shows.forced, table_shows.audio_language, table_shows.seriesType FROM " + "table_episodes LEFT JOIN table_shows on table_episodes.sonarrSeriesId = " + "table_shows.sonarrSeriesId WHERE sonarrEpisodeId=?", (no,)) episodes_details = filter_exclusions(episodes_details, 'series') if not episodes_details: logging.debug("BAZARR no episode with that sonarrEpisodeId can be found in database:", str(no)) @@ -887,9 +887,10 @@ def wanted_download_subtitles_movie(path, l, count_movies): def wanted_search_missing_subtitles_series(): - episodes = database.execute("SELECT table_episodes.path, table_shows.tags, table_episodes.monitored FROM " - "table_episodes INNER JOIN table_shows on table_shows.sonarrSeriesId = " - "table_episodes.sonarrSeriesId WHERE missing_subtitles != '[]'") + episodes = database.execute("SELECT table_episodes.path, table_shows.tags, table_episodes.monitored, " + "table_shows.seriesType FROM table_episodes INNER JOIN table_shows on " + "table_shows.sonarrSeriesId = table_episodes.sonarrSeriesId WHERE missing_subtitles != " + "'[]'") episodes = filter_exclusions(episodes, 'series') # path_replace dict_mapper.path_replace(episodes) @@ -1048,12 +1049,12 @@ def upgrade_subtitles(): if settings.general.getboolean('use_sonarr'): upgradable_episodes = database.execute("SELECT table_history.video_path, table_history.language, " - "table_history.score, table_shows.hearing_impaired, table_shows.audio_language, " - "table_episodes.scene_name, table_episodes.title," + "table_history.score, table_shows.hearing_impaired, " + "table_shows.audio_language, table_episodes.scene_name, table_episodes.title," "table_episodes.sonarrSeriesId, table_episodes.sonarrEpisodeId," "MAX(table_history.timestamp) as timestamp, table_episodes.monitored, " - "table_shows.languages, table_shows.forced, table_shows.tags " - "FROM table_history INNER JOIN table_shows on " + "table_shows.languages, table_shows.forced, table_shows.tags, " + "table_shows.seriesType FROM table_history INNER JOIN table_shows on " "table_shows.sonarrSeriesId = table_history.sonarrSeriesId INNER JOIN " "table_episodes on table_episodes.sonarrEpisodeId = " "table_history.sonarrEpisodeId WHERE action IN " diff --git a/static/css/tagsinput.css b/static/css/tagsinput.css new file mode 100644 index 000000000..c2e6e3526 --- /dev/null +++ b/static/css/tagsinput.css @@ -0,0 +1,103 @@ +/* + * bootstrap-tagsinput v0.8.0 + * + */ + +.bootstrap-tagsinput { + background-color: #fff; + border: 1px solid #ccc; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + display: inline-block; + padding: 4px 6px; + color: #555; + vertical-align: middle; + border-radius: 4px; + width: 100%; + line-height: 22px; + cursor: text; +} +.bootstrap-tagsinput input { + border: none; + box-shadow: none; + outline: none; + background-color: transparent; + padding: 0 6px; + margin: 0; + width: auto; + max-width: inherit; +} +.bootstrap-tagsinput.form-control input::-moz-placeholder { + color: #777; + opacity: 1; +} +.bootstrap-tagsinput.form-control input:-ms-input-placeholder { + color: #777; +} +.bootstrap-tagsinput.form-control input::-webkit-input-placeholder { + color: #777; +} +.bootstrap-tagsinput input:focus { + border: none; + box-shadow: none; +} +.bootstrap-tagsinput .badge { + margin: 2px 0; + padding:5px 8px; +} +.bootstrap-tagsinput .badge [data-role="remove"] { + margin-left: 8px; + cursor: pointer; +} +.bootstrap-tagsinput .badge [data-role="remove"]:after { + content: "×"; + padding: 0px 4px; + background-color:rgba(0, 0, 0, 0.1); + border-radius:50%; + font-size:13px +} +.bootstrap-tagsinput .badge [data-role="remove"]:hover:after { + + background-color:rgba(0, 0, 0, 0.62);} +.bootstrap-tagsinput .badge [data-role="remove"]:hover:active { + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} + +.tt-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + list-style: none; + font-size: 14px; + background-color: #ffffff; + border: 1px solid #cccccc; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 4px; + -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + background-clip: padding-box; + cursor: pointer; +} + +.tt-suggestion { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 1.428571429; + color: #333333; + white-space: nowrap; +} + +.tt-suggestion:hover, +.tt-suggestion:focus { + color: #ffffff; + text-decoration: none; + outline: 0; + background-color: #428bca; +} diff --git a/static/js/tagsinput.js b/static/js/tagsinput.js new file mode 100644 index 000000000..7afb3c928 --- /dev/null +++ b/static/js/tagsinput.js @@ -0,0 +1,691 @@ +/* + * bootstrap-tagsinput v0.8.0 + * + */ + +(function ($) { + "use strict"; + + var defaultOptions = { + tagClass: function(item) { + return 'badge badge-info'; + }, + focusClass: 'focus', + itemValue: function(item) { + return item ? item.toString() : item; + }, + itemText: function(item) { + return this.itemValue(item); + }, + itemTitle: function(item) { + return null; + }, + freeInput: true, + addOnBlur: true, + maxTags: undefined, + maxChars: undefined, + confirmKeys: [13, 44], + delimiter: ',', + delimiterRegex: null, + cancelConfirmKeysOnEmpty: false, + onTagExists: function(item, $tag) { + $tag.hide().fadeIn(); + }, + trimValue: false, + allowDuplicates: false, + triggerChange: true, + editOnBackspace: false + }; + + /** + * Constructor function + */ + function TagsInput(element, options) { + this.isInit = true; + this.itemsArray = []; + + this.$element = $(element); + this.$element.addClass('sr-only'); + + this.isSelect = (element.tagName === 'SELECT'); + this.multiple = (this.isSelect && element.hasAttribute('multiple')); + this.objectItems = options && options.itemValue; + this.placeholderText = element.hasAttribute('placeholder') ? this.$element.attr('placeholder') : ''; + this.inputSize = Math.max(1, this.placeholderText.length); + + this.$container = $('
'); + this.$input = $('').appendTo(this.$container); + + this.$element.before(this.$container); + + this.build(options); + this.isInit = false; + } + + TagsInput.prototype = { + constructor: TagsInput, + + /** + * Adds the given item as a new tag. Pass true to dontPushVal to prevent + * updating the elements val() + */ + add: function(item, dontPushVal, options) { + var self = this; + + if (self.options.maxTags && self.itemsArray.length >= self.options.maxTags) + return; + + // Ignore falsey values, except false + if (item !== false && !item) + return; + + // Trim value + if (typeof item === "string" && self.options.trimValue) { + item = $.trim(item); + } + + // Throw an error when trying to add an object while the itemValue option was not set + if (typeof item === "object" && !self.objectItems) + throw("Can't add objects when itemValue option is not set"); + + // Ignore strings only containg whitespace + if (item.toString().match(/^\s*$/)) + return; + + // If SELECT but not multiple, remove current tag + if (self.isSelect && !self.multiple && self.itemsArray.length > 0) + self.remove(self.itemsArray[0]); + + if (typeof item === "string" && this.$element[0].tagName === 'INPUT') { + var delimiter = (self.options.delimiterRegex) ? self.options.delimiterRegex : self.options.delimiter; + var items = item.split(delimiter); + if (items.length > 1) { + for (var i = 0; i < items.length; i++) { + this.add(items[i], true); + } + + if (!dontPushVal) + self.pushVal(self.options.triggerChange); + return; + } + } + + var itemValue = self.options.itemValue(item), + itemText = self.options.itemText(item), + tagClass = self.options.tagClass(item), + itemTitle = self.options.itemTitle(item); + + // Ignore items allready added + var existing = $.grep(self.itemsArray, function(item) { return self.options.itemValue(item) === itemValue; } )[0]; + if (existing && !self.options.allowDuplicates) { + // Invoke onTagExists + if (self.options.onTagExists) { + var $existingTag = $(".badge", self.$container).filter(function() { return $(this).data("item") === existing; }); + self.options.onTagExists(item, $existingTag); + } + return; + } + + // if length greater than limit + if (self.items().toString().length + item.length + 1 > self.options.maxInputLength) + return; + + // raise beforeItemAdd arg + var beforeItemAddEvent = $.Event('beforeItemAdd', { item: item, cancel: false, options: options}); + self.$element.trigger(beforeItemAddEvent); + if (beforeItemAddEvent.cancel) + return; + + // register item in internal array and map + self.itemsArray.push(item); + + // add a tag element + + var $tag = $('' + htmlEncode(itemText) + ''); + $tag.data('item', item); + self.findInputWrapper().before($tag); + + // Check to see if the tag exists in its raw or uri-encoded form + var optionExists = ( + $('option[value="' + encodeURIComponent(itemValue).replace(/"/g, '\\"') + '"]', self.$element).length || + $('option[value="' + htmlEncode(itemValue).replace(/"/g, '\\"') + '"]', self.$element).length + ); + + // add