1# This file is part of Tautulli.
2#
3#  Tautulli is free software: you can redistribute it and/or modify
4#  it under the terms of the GNU General Public License as published by
5#  the Free Software Foundation, either version 3 of the License, or
6#  (at your option) any later version.
7#
8#  Tautulli is distributed in the hope that it will be useful,
9#  but WITHOUT ANY WARRANTY; without even the implied warranty of
10#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11#  GNU General Public License for more details.
12#
13#  You should have received a copy of the GNU General Public License
14#  along with Tautulli.  If not, see <http://www.gnu.org/licenses/>.
15
16from __future__ import unicode_literals
17from future.builtins import object
18from future.builtins import str
19
20import os
21import re
22import shutil
23import time
24import threading
25
26from configobj import ConfigObj, ParseError
27from hashing_passwords import make_hash
28
29import plexpy
30if plexpy.PYTHON2:
31    import helpers
32    import logger
33else:
34    from plexpy import helpers
35    from plexpy import logger
36
37
38def bool_int(value):
39    """
40    Casts a config value into a 0 or 1
41    """
42    if isinstance(value, str):
43        if value.lower() in ('', '0', 'false', 'f', 'no', 'n', 'off'):
44            value = 0
45    return int(bool(value))
46
47
48FILENAME = "config.ini"
49
50_CONFIG_DEFINITIONS = {
51    'ALLOW_GUEST_ACCESS': (int, 'General', 0),
52    'DATE_FORMAT': (str, 'General', 'YYYY-MM-DD'),
53    'PMS_CLIENT_ID': (str, 'PMS', ''),
54    'PMS_IDENTIFIER': (str, 'PMS', ''),
55    'PMS_IP': (str, 'PMS', '127.0.0.1'),
56    'PMS_IS_CLOUD': (int, 'PMS', 0),
57    'PMS_IS_REMOTE': (int, 'PMS', 0),
58    'PMS_LOGS_FOLDER': (str, 'PMS', ''),
59    'PMS_LOGS_LINE_CAP': (int, 'PMS', 1000),
60    'PMS_NAME': (str, 'PMS', ''),
61    'PMS_PORT': (int, 'PMS', 32400),
62    'PMS_TOKEN': (str, 'PMS', ''),
63    'PMS_SSL': (int, 'PMS', 0),
64    'PMS_URL': (str, 'PMS', ''),
65    'PMS_URL_OVERRIDE': (str, 'PMS', ''),
66    'PMS_URL_MANUAL': (int, 'PMS', 0),
67    'PMS_USE_BIF': (int, 'PMS', 0),
68    'PMS_UUID': (str, 'PMS', ''),
69    'PMS_TIMEOUT': (int, 'Advanced', 15),
70    'PMS_PLEXPASS': (int, 'PMS', 0),
71    'PMS_PLATFORM': (str, 'PMS', ''),
72    'PMS_VERSION': (str, 'PMS', ''),
73    'PMS_UPDATE_CHANNEL': (str, 'PMS', 'plex'),
74    'PMS_UPDATE_DISTRO': (str, 'PMS', ''),
75    'PMS_UPDATE_DISTRO_BUILD': (str, 'PMS', ''),
76    'PMS_UPDATE_CHECK_INTERVAL': (int, 'Advanced', 24),
77    'PMS_WEB_URL': (str, 'PMS', 'https://app.plex.tv/desktop'),
78    'TIME_FORMAT': (str, 'General', 'HH:mm'),
79    'ANON_REDIRECT': (str, 'General', ''),
80    'ANON_REDIRECT_DYNAMIC': (int, 'General', 1),
81    'API_ENABLED': (int, 'General', 1),
82    'API_KEY': (str, 'General', ''),
83    'API_SQL': (int, 'General', 0),
84    'BUFFER_THRESHOLD': (int, 'Monitoring', 10),
85    'BUFFER_WAIT': (int, 'Monitoring', 900),
86    'BACKUP_DAYS': (int, 'General', 3),
87    'BACKUP_DIR': (str, 'General', ''),
88    'BACKUP_INTERVAL': (int, 'General', 6),
89    'CACHE_DIR': (str, 'General', ''),
90    'CACHE_IMAGES': (int, 'General', 1),
91    'CACHE_SIZEMB': (int, 'Advanced', 32),
92    'CHECK_DOCKER_MOUNT': (int, 'Advanced', 1),
93    'CHECK_GITHUB': (int, 'General', 0),
94    'CHECK_GITHUB_INTERVAL': (int, 'General', 360),
95    'CHECK_GITHUB_ON_STARTUP': (int, 'General', 1),
96    'CHECK_GITHUB_CACHE_SECONDS': (int, 'Advanced', 3600),
97    'CLEANUP_FILES': (int, 'General', 0),
98    'CLOUDINARY_CLOUD_NAME': (str, 'Cloudinary', ''),
99    'CLOUDINARY_API_KEY': (str, 'Cloudinary', ''),
100    'CLOUDINARY_API_SECRET': (str, 'Cloudinary', ''),
101    'CONFIG_VERSION': (int, 'Advanced', 0),
102    'DO_NOT_OVERRIDE_GIT_BRANCH': (int, 'General', 0),
103    'ENABLE_HTTPS': (int, 'General', 0),
104    'EXPORT_DIR': (str, 'General', ''),
105    'EXPORT_THREADS': (int, 'Advanced', 8),
106    'FIRST_RUN_COMPLETE': (int, 'General', 0),
107    'FREEZE_DB': (int, 'General', 0),
108    'GET_FILE_SIZES': (int, 'General', 0),
109    'GET_FILE_SIZES_HOLD': (dict, 'General', {'section_ids': [], 'rating_keys': []}),
110    'GIT_BRANCH': (str, 'General', 'master'),
111    'GIT_PATH': (str, 'General', ''),
112    'GIT_REMOTE': (str, 'General', 'origin'),
113    'GIT_TOKEN': (str, 'General', ''),
114    'GIT_USER': (str, 'General', 'Tautulli'),
115    'GIT_REPO': (str, 'General', 'Tautulli'),
116    'GROUP_HISTORY_TABLES': (int, 'General', 1),
117    'HISTORY_TABLE_ACTIVITY': (int, 'General', 1),
118    'HOME_SECTIONS': (list, 'General', ['current_activity', 'watch_stats', 'library_stats', 'recently_added']),
119    'HOME_LIBRARY_CARDS': (list, 'General', ['first_run']),
120    'HOME_STATS_CARDS': (list, 'General', ['top_movies', 'popular_movies', 'top_tv', 'popular_tv', 'top_music',
121        'popular_music', 'last_watched', 'top_libraries', 'top_users', 'top_platforms', 'most_concurrent']),
122    'HOME_REFRESH_INTERVAL': (int, 'General', 10),
123    'HTTPS_CREATE_CERT': (int, 'General', 1),
124    'HTTPS_CERT': (str, 'General', ''),
125    'HTTPS_CERT_CHAIN': (str, 'General', ''),
126    'HTTPS_KEY': (str, 'General', ''),
127    'HTTPS_DOMAIN': (str, 'General', 'localhost'),
128    'HTTPS_IP': (str, 'General', '127.0.0.1'),
129    'HTTP_BASIC_AUTH': (int, 'General', 0),
130    'HTTP_ENVIRONMENT': (str, 'General', 'production'),
131    'HTTP_HASH_PASSWORD': (int, 'General', 1),
132    'HTTP_HASHED_PASSWORD': (int, 'General', 1),
133    'HTTP_HOST': (str, 'General', '0.0.0.0'),
134    'HTTP_PASSWORD': (str, 'General', ''),
135    'HTTP_PORT': (int, 'General', 8181),
136    'HTTP_PROXY': (int, 'General', 0),
137    'HTTP_ROOT': (str, 'General', ''),
138    'HTTP_USERNAME': (str, 'General', ''),
139    'HTTP_PLEX_ADMIN': (int, 'General', 0),
140    'HTTP_BASE_URL': (str, 'General', ''),
141    'HTTP_RATE_LIMIT_ATTEMPTS': (int, 'General', 10),
142    'HTTP_RATE_LIMIT_ATTEMPTS_INTERVAL': (int, 'General', 300),
143    'HTTP_RATE_LIMIT_LOCKOUT_TIME': (int, 'General', 300),
144    'HTTP_THREAD_POOL': (int, 'General', 10),
145    'INTERFACE': (str, 'General', 'default'),
146    'IMGUR_CLIENT_ID': (str, 'Monitoring', ''),
147    'JOURNAL_MODE': (str, 'Advanced', 'WAL'),
148    'LAUNCH_BROWSER': (int, 'General', 1),
149    'LAUNCH_STARTUP': (int, 'General', 1),
150    'LOG_BLACKLIST': (int, 'General', 1),
151    'LOG_DIR': (str, 'General', ''),
152    'LOGGING_IGNORE_INTERVAL': (int, 'Monitoring', 120),
153    'METADATA_CACHE_SECONDS': (int, 'Advanced', 1800),
154    'MOVIE_WATCHED_PERCENT': (int, 'Monitoring', 85),
155    'MUSIC_WATCHED_PERCENT': (int, 'Monitoring', 85),
156    'MUSICBRAINZ_LOOKUP': (int, 'General', 0),
157    'MONITOR_PMS_UPDATES': (int, 'Monitoring', 0),
158    'MONITORING_INTERVAL': (int, 'Monitoring', 60),
159    'NEWSLETTER_AUTH': (int, 'Newsletter', 0),
160    'NEWSLETTER_PASSWORD': (str, 'Newsletter', ''),
161    'NEWSLETTER_CUSTOM_DIR': (str, 'Newsletter', ''),
162    'NEWSLETTER_INLINE_STYLES': (int, 'Newsletter', 1),
163    'NEWSLETTER_TEMPLATES': (str, 'Newsletter', 'newsletters'),
164    'NEWSLETTER_DIR': (str, 'Newsletter', ''),
165    'NEWSLETTER_SELF_HOSTED': (int, 'Newsletter', 0),
166    'NOTIFICATION_THREADS': (int, 'Advanced', 2),
167    'NOTIFY_CONSECUTIVE': (int, 'Monitoring', 1),
168    'NOTIFY_CONTINUED_SESSION_THRESHOLD': (int, 'Monitoring', 15),
169    'NOTIFY_GROUP_RECENTLY_ADDED_GRANDPARENT': (int, 'Monitoring', 1),
170    'NOTIFY_GROUP_RECENTLY_ADDED_PARENT': (int, 'Monitoring', 1),
171    'NOTIFY_UPLOAD_POSTERS': (int, 'Monitoring', 0),
172    'NOTIFY_RECENTLY_ADDED_DELAY': (int, 'Monitoring', 300),
173    'NOTIFY_RECENTLY_ADDED_GRANDPARENT': (int, 'Monitoring', 0),
174    'NOTIFY_RECENTLY_ADDED_UPGRADE': (int, 'Monitoring', 0),
175    'NOTIFY_REMOTE_ACCESS_THRESHOLD': (int, 'Monitoring', 60),
176    'NOTIFY_CONCURRENT_BY_IP': (int, 'Monitoring', 0),
177    'NOTIFY_CONCURRENT_THRESHOLD': (int, 'Monitoring', 2),
178    'NOTIFY_NEW_DEVICE_INITIAL_ONLY': (int, 'Monitoring', 1),
179    'NOTIFY_SERVER_CONNECTION_THRESHOLD': (int, 'Monitoring', 60),
180    'NOTIFY_SERVER_UPDATE_REPEAT': (int, 'Monitoring', 0),
181    'NOTIFY_PLEXPY_UPDATE_REPEAT': (int, 'Monitoring', 0),
182    'NOTIFY_TEXT_EVAL': (int, 'Advanced', 0),
183    'PLEXPY_AUTO_UPDATE': (int, 'General', 0),
184    'REFRESH_LIBRARIES_INTERVAL': (int, 'Monitoring', 12),
185    'REFRESH_LIBRARIES_ON_STARTUP': (int, 'Monitoring', 1),
186    'REFRESH_USERS_INTERVAL': (int, 'Monitoring', 12),
187    'REFRESH_USERS_ON_STARTUP': (int, 'Monitoring', 1),
188    'SESSION_DB_WRITE_ATTEMPTS': (int, 'Advanced', 5),
189    'SHOW_ADVANCED_SETTINGS': (int, 'General', 0),
190    'SYNCHRONOUS_MODE': (str, 'Advanced', 'NORMAL'),
191    'THEMOVIEDB_APIKEY': (str, 'General', 'e9a6655bae34bf694a0f3e33338dc28e'),
192    'THEMOVIEDB_LOOKUP': (int, 'General', 0),
193    'TVMAZE_LOOKUP': (int, 'General', 0),
194    'TV_WATCHED_PERCENT': (int, 'Monitoring', 85),
195    'UPDATE_DB_INTERVAL': (int, 'General', 24),
196    'UPDATE_SHOW_CHANGELOG': (int, 'General', 1),
197    'UPGRADE_FLAG': (int, 'Advanced', 0),
198    'VERBOSE_LOGS': (int, 'Advanced', 1),
199    'VERIFY_SSL_CERT': (bool_int, 'Advanced', 1),
200    'WEBSOCKET_MONITOR_PING_PONG': (int, 'Advanced', 0),
201    'WEBSOCKET_CONNECTION_ATTEMPTS': (int, 'Advanced', 5),
202    'WEBSOCKET_CONNECTION_TIMEOUT': (int, 'Advanced', 5),
203    'WEEK_START_MONDAY': (int, 'General', 0),
204    'JWT_SECRET': (str, 'Advanced', ''),
205    'JWT_UPDATE_SECRET': (bool_int, 'Advanced', 0),
206    'SYSTEM_ANALYTICS': (int, 'Advanced', 1),
207    'SYS_TRAY_ICON': (int, 'General', 1),
208}
209
210_BLACKLIST_KEYS = ['_APITOKEN', '_TOKEN', '_KEY', '_SECRET', '_PASSWORD', '_APIKEY', '_ID', '_HOOK']
211_WHITELIST_KEYS = ['HTTPS_KEY']
212
213_DO_NOT_IMPORT_KEYS = [
214    'FIRST_RUN_COMPLETE', 'GET_FILE_SIZES_HOLD', 'GIT_PATH', 'PMS_LOGS_FOLDER',
215    'BACKUP_DIR', 'CACHE_DIR', 'EXPORT_DIR', 'LOG_DIR', 'NEWSLETTER_DIR', 'NEWSLETTER_CUSTOM_DIR',
216    'HTTP_HOST', 'HTTP_PORT', 'HTTP_ROOT',
217    'HTTP_USERNAME', 'HTTP_PASSWORD', 'HTTP_HASH_PASSWORD', 'HTTP_HASHED_PASSWORD',
218    'ENABLE_HTTPS', 'HTTPS_CREATE_CERT', 'HTTPS_CERT', 'HTTPS_CERT_CHAIN', 'HTTPS_KEY'
219]
220_DO_NOT_IMPORT_KEYS_DOCKER = [
221    'PLEXPY_AUTO_UPDATE', 'GIT_REMOTE', 'GIT_BRANCH'
222]
223
224IS_IMPORTING = False
225IMPORT_THREAD = None
226
227
228def set_is_importing(value):
229    global IS_IMPORTING
230    IS_IMPORTING = value
231
232
233def set_import_thread(config=None, backup=False):
234    global IMPORT_THREAD
235    if config:
236        if IMPORT_THREAD:
237            return
238        IMPORT_THREAD = threading.Thread(target=import_tautulli_config,
239                                         kwargs={'config': config, 'backup': backup})
240    else:
241        IMPORT_THREAD = None
242
243
244def import_tautulli_config(config=None, backup=False):
245    if IS_IMPORTING:
246        logger.warn("Tautulli Config :: Another Tautulli config is currently being imported. "
247                    "Please wait until it is complete before importing another config.")
248        return False
249
250    if backup:
251        # Make a backup of the current config first
252        logger.info("Tautulli Config :: Creating a config backup before importing.")
253        if not make_backup():
254            logger.error("Tautulli Config :: Failed to import Tautulli config: failed to create config backup")
255            return False
256
257    # Create a new Config object with the imported config file
258    try:
259        imported_config = Config(config, is_import=True)
260    except:
261        logger.error("Tautulli Config :: Failed to import Tautulli config: error reading imported config file")
262        return False
263
264    logger.info("Tautulli Config :: Importing Tautulli config '%s'...", config)
265    set_is_importing(True)
266
267    # Remove keys that should not be imported
268    for key in _DO_NOT_IMPORT_KEYS:
269        delattr(imported_config, key)
270    if plexpy.DOCKER or plexpy.SNAP:
271        for key in _DO_NOT_IMPORT_KEYS_DOCKER:
272            delattr(imported_config, key)
273
274    # Merge the imported config file into the current config file
275    plexpy.CONFIG._config.merge(imported_config._config)
276    plexpy.CONFIG.write()
277
278    logger.info("Tautulli Config :: Tautulli config import complete.")
279    set_import_thread(None)
280    set_is_importing(False)
281
282    # Restart to apply changes
283    plexpy.SIGNAL = 'restart'
284
285
286def make_backup(cleanup=False, scheduler=False):
287    """ Makes a backup of config file, removes all but the last 5 backups """
288
289    if scheduler:
290        backup_file = 'config.backup-{}.sched.ini'.format(helpers.now())
291    else:
292        backup_file = 'config.backup-{}.ini'.format(helpers.now())
293    backup_folder = plexpy.CONFIG.BACKUP_DIR
294    backup_file_fp = os.path.join(backup_folder, backup_file)
295
296    # In case the user has deleted it manually
297    if not os.path.exists(backup_folder):
298        os.makedirs(backup_folder)
299
300    plexpy.CONFIG.write()
301    shutil.copyfile(plexpy.CONFIG_FILE, backup_file_fp)
302
303    if cleanup:
304        now = time.time()
305        # Delete all scheduled backup older than BACKUP_DAYS.
306        for root, dirs, files in os.walk(backup_folder):
307            ini_files = [os.path.join(root, f) for f in files if f.endswith('.sched.ini')]
308            for file_ in ini_files:
309                if os.stat(file_).st_mtime < now - plexpy.CONFIG.BACKUP_DAYS * 86400:
310                    try:
311                        os.remove(file_)
312                    except OSError as e:
313                        logger.error("Tautulli Config :: Failed to delete %s from the backup folder: %s" % (file_, e))
314
315    if backup_file in os.listdir(backup_folder):
316        logger.debug("Tautulli Config :: Successfully backed up %s to %s" % (plexpy.CONFIG_FILE, backup_file))
317        return True
318    else:
319        logger.error("Tautulli Config :: Failed to backup %s to %s" % (plexpy.CONFIG_FILE, backup_file))
320        return False
321
322
323# pylint:disable=R0902
324# it might be nice to refactor for fewer instance variables
325class Config(object):
326    """ Wraps access to particular values in a config file """
327
328    def __init__(self, config_file, is_import=False):
329        """ Initialize the config with values from a file """
330        self._config_file = config_file
331        try:
332            self._config = ConfigObj(self._config_file, encoding='utf-8')
333        except ParseError as e:
334            logger.error("Tautulli Config :: Error reading configuration file: %s", e)
335            raise
336
337        for key in _CONFIG_DEFINITIONS:
338            self.check_setting(key)
339        if not is_import:
340            self._upgrade()
341            self._blacklist()
342
343    def _blacklist(self):
344        """ Add tokens and passwords to blacklisted words in logger """
345        blacklist = set()
346
347        for key, subkeys in self._config.items():
348            for subkey, value in subkeys.items():
349                if isinstance(value, str) and len(value.strip()) > 5 and \
350                    subkey.upper() not in _WHITELIST_KEYS and any(bk in subkey.upper() for bk in _BLACKLIST_KEYS):
351                    blacklist.add(value.strip())
352
353        logger._BLACKLIST_WORDS.update(blacklist)
354
355    def _define(self, name):
356        key = name.upper()
357        ini_key = name.lower()
358        definition = _CONFIG_DEFINITIONS[key]
359        if len(definition) == 3:
360            definition_type, section, default = definition
361        else:
362            definition_type, section, _, default = definition
363        return key, definition_type, section, ini_key, default
364
365    def check_section(self, section):
366        """ Check if INI section exists, if not create it """
367        if section not in self._config:
368            self._config[section] = {}
369            return True
370        else:
371            return False
372
373    def check_setting(self, key):
374        """ Cast any value in the config to the right type or use the default """
375        key, definition_type, section, ini_key, default = self._define(key)
376        self.check_section(section)
377        try:
378            my_val = definition_type(self._config[section][ini_key])
379        except Exception:
380            my_val = definition_type(default)
381            self._config[section][ini_key] = my_val
382        return my_val
383
384    def write(self):
385        """ Make a copy of the stored config and write it to the configured file """
386        new_config = ConfigObj(encoding="UTF-8")
387        new_config.filename = self._config_file
388
389        # first copy over everything from the old config, even if it is not
390        # correctly defined to keep from losing data
391        for key, subkeys in self._config.items():
392            if key not in new_config:
393                new_config[key] = {}
394            for subkey, value in subkeys.items():
395                new_config[key][subkey] = value
396
397        # next make sure that everything we expect to have defined is so
398        for key in _CONFIG_DEFINITIONS:
399            key, definition_type, section, ini_key, default = self._define(key)
400            self.check_setting(key)
401            if section not in new_config:
402                new_config[section] = {}
403            new_config[section][ini_key] = self._config[section][ini_key]
404
405        # Write it to file
406        logger.info("Tautulli Config :: Writing configuration to file")
407
408        try:
409            new_config.write()
410        except IOError as e:
411            logger.error("Tautulli Config :: Error writing configuration file: %s", e)
412
413        self._blacklist()
414
415    def __getattr__(self, name):
416        """
417        Returns something from the ini unless it is a real property
418        of the configuration object or is not all caps.
419        """
420        if not re.match(r'[A-Z_]+$', name):
421            return super(Config, self).__getattr__(name)
422        else:
423            return self.check_setting(name)
424
425    def __setattr__(self, name, value):
426        """
427        Maps all-caps properties to ini values unless they exist on the
428        configuration object.
429        """
430        if not re.match(r'[A-Z_]+$', name):
431            super(Config, self).__setattr__(name, value)
432            return value
433        else:
434            key, definition_type, section, ini_key, default = self._define(name)
435            self._config[section][ini_key] = definition_type(value)
436            return self._config[section][ini_key]
437
438    def __delattr__(self, name):
439        """
440        Deletes a key from the configuration object.
441        """
442        if not re.match(r'[A-Z_]+$', name):
443            return super(Config, self).__delattr__(name)
444        else:
445            key, definition_type, section, ini_key, default = self._define(name)
446            del self._config[section][ini_key]
447
448    def process_kwargs(self, kwargs):
449        """
450        Given a big bunch of key value pairs, apply them to the ini.
451        """
452        for name, value in kwargs.items():
453            key, definition_type, section, ini_key, default = self._define(name)
454            self._config[section][ini_key] = definition_type(value)
455
456    def _upgrade(self):
457        """
458        Upgrades config file from previous verisions and bumps up config version
459        """
460        if self.CONFIG_VERSION == 0:
461            self.CONFIG_VERSION = 1
462
463        if self.CONFIG_VERSION == 1:
464            # Change home_stats_cards to list
465            if self.HOME_STATS_CARDS:
466                home_stats_cards = ''.join(self.HOME_STATS_CARDS).split(', ')
467                if 'watch_statistics' in home_stats_cards:
468                    home_stats_cards.remove('watch_statistics')
469                    self.HOME_STATS_CARDS = home_stats_cards
470            # Change home_library_cards to list
471            if self.HOME_LIBRARY_CARDS:
472                home_library_cards = ''.join(self.HOME_LIBRARY_CARDS).split(', ')
473                if 'library_statistics' in home_library_cards:
474                    home_library_cards.remove('library_statistics')
475                    self.HOME_LIBRARY_CARDS = home_library_cards
476
477            self.CONFIG_VERSION = 2
478
479        if self.CONFIG_VERSION == 2:
480            self.CONFIG_VERSION = 3
481
482        if self.CONFIG_VERSION == 3:
483            if self.HTTP_ROOT == '/':
484                self.HTTP_ROOT = ''
485
486            self.CONFIG_VERSION = 4
487
488        if self.CONFIG_VERSION == 4:
489            if not len(self.HOME_STATS_CARDS) and 'watch_stats' in self.HOME_SECTIONS:
490                home_sections = self.HOME_SECTIONS
491                home_sections.remove('watch_stats')
492                self.HOME_SECTIONS = home_sections
493            if not len(self.HOME_LIBRARY_CARDS) and 'library_stats' in self.HOME_SECTIONS:
494                home_sections = self.HOME_SECTIONS
495                home_sections.remove('library_stats')
496                self.HOME_SECTIONS = home_sections
497
498            self.CONFIG_VERSION = 5
499
500        if self.CONFIG_VERSION == 5:
501            self.MONITOR_PMS_UPDATES = 0
502
503            self.CONFIG_VERSION = 6
504
505        if self.CONFIG_VERSION == 6:
506            if self.GIT_USER.lower() == 'drzoidberg33':
507                self.GIT_USER = 'JonnyWong16'
508
509            self.CONFIG_VERSION = 7
510
511        if self.CONFIG_VERSION == 7:
512            self.CONFIG_VERSION = 8
513
514        if self.CONFIG_VERSION == 8:
515            self.CONFIG_VERSION = 9
516
517        if self.CONFIG_VERSION == 9:
518            if self.PMS_UPDATE_CHANNEL == 'plexpass':
519                self.PMS_UPDATE_CHANNEL = 'beta'
520
521            self.CONFIG_VERSION = 10
522
523        if self.CONFIG_VERSION == 10:
524            self.GIT_USER = 'Tautulli'
525            self.GIT_REPO = 'Tautulli'
526
527            self.CONFIG_VERSION = 11
528
529        if self.CONFIG_VERSION == 11:
530            self.ANON_REDIRECT = self.ANON_REDIRECT.replace('http://www.nullrefer.com/?',
531                                                            'https://www.nullrefer.com/?')
532            self.CONFIG_VERSION = 12
533
534        if self.CONFIG_VERSION == 12:
535            self.BUFFER_THRESHOLD = max(self.BUFFER_THRESHOLD, 10)
536
537            self.CONFIG_VERSION = 13
538
539        if self.CONFIG_VERSION == 13:
540            self.CONFIG_VERSION = 14
541
542        if self.CONFIG_VERSION == 14:
543            if plexpy.DOCKER:
544                self.PLEXPY_AUTO_UPDATE = 0
545
546            self.CONFIG_VERSION = 15
547
548        if self.CONFIG_VERSION == 15:
549            if self.HTTP_ROOT and self.HTTP_ROOT != '/':
550                self.JWT_UPDATE_SECRET = True
551
552            self.CONFIG_VERSION = 16
553
554        if self.CONFIG_VERSION == 16:
555            if plexpy.SNAP:
556                self.PLEXPY_AUTO_UPDATE = 0
557
558            self.CONFIG_VERSION = 17
559
560        if self.CONFIG_VERSION == 17:
561            home_stats_cards = self.HOME_STATS_CARDS
562            if 'top_users' in home_stats_cards:
563                top_users_index = home_stats_cards.index('top_users')
564                home_stats_cards.insert(top_users_index, 'top_libraries')
565            else:
566                home_stats_cards.append('top_libraries')
567            self.HOME_STATS_CARDS = home_stats_cards
568
569            self.CONFIG_VERSION = 18
570
571        if self.CONFIG_VERSION == 18:
572            self.CHECK_GITHUB_INTERVAL = (
573                    int(self.CHECK_GITHUB_INTERVAL // 60)
574                    + (self.CHECK_GITHUB_INTERVAL % 60 > 0)
575            )
576
577            self.CONFIG_VERSION = 19
578
579        if self.CONFIG_VERSION == 19:
580            if self.HTTP_PASSWORD and not self.HTTP_HASHED_PASSWORD:
581                self.HTTP_PASSWORD = make_hash(self.HTTP_PASSWORD)
582            self.HTTP_HASH_PASSWORD = 1
583            self.HTTP_HASHED_PASSWORD = 1
584
585            self.CONFIG_VERSION = 20
586