├── README.md ├── database-migration.py ├── jellyseerr └── .gitkeep └── overseerr └── .gitkeep /README.md: -------------------------------------------------------------------------------- 1 | # Migrate Overseerr settings and database to Jellyseerr 2 | 3 | This script migrates the settings and database from Overseerr to Jellyseerr. It is intended for Plex users who have been using Overseerr and want to switch to Jellyseerr without losing data. As the database structure of Overseerr and Jellyseerr are a bit different, it is unfortunately not possible to simply copy the database file from Overseerr to Jellyseerr. 4 | 5 | The script will migrate your settings, media, users, requests and issues from Overseerr to the Jellyseerr by populating the Jellyseerr `settings.json` file with the values from Overseerr and injecting the data records from the `user`, `user_settings`, `user_push_subscription`, `media`, `media_request`, `season`, `season_request`, `issue` and `issue_comment` tables from the Overseerr database into the according tables in the Jellyseerr database. 6 | 7 | 1. Clone this repository or set up the the directory structure manually as shown below. 8 | 2. Install Jellyseerr. You don't need to configure it. 9 | 3. Stop Jellyseerr and Overseerr. 10 | 4. Copy your Overseerr and Jellyseerr `db` folders and `settings.json` files to `./overseerr` and `./jellyseerr` respectively. The structure should look like this: 11 | ```bash 12 | . 13 | ├── database-migration.py 14 | ├── jellyseerr 15 | │   ├── db 16 | │   │   ├── db.sqlite3 17 | │   │   ├── db.sqlite3-shm # May or may not exist. 18 | │   │   └── db.sqlite3-wal # Please copy the -shm and -wal files if they exist. 19 | │   └── settings.json 20 | └── overseerr 21 | ├── db 22 | │   ├── db.sqlite3 23 | │   ├── db.sqlite3-shm # Same as above. 24 | │   └── db.sqlite3-wal # 25 | └── settings.json 26 | ``` 27 | 5. Run the following command to migrate the database: 28 | ```bash 29 | python3 database-migration.py 30 | ``` 31 | 6. Copy the Jellyseerr `db.sqlite3` database file back to the Jellyseerr database directory. Delete `db.sqlite3-shm` and `db.sqlite3-wal` files if they exist. Also copy the Jellyseerr `settings.json` file back to the config directory. 32 | 7. Start Jellyseerr and verify that everything is working as expected. 33 | 34 | In case of any issues, there are copies of the original Overseerr and Jellyseerr database and settings files in the `./backup` directory. 35 | 36 | -------------------------------------------------------------------------------- /database-migration.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import json 3 | import shutil 4 | import os 5 | 6 | overseerr_config_path = './overseerr' 7 | jellyseerr_config_path = './jellyseerr' 8 | 9 | overseerr_db_path = os.path.join(overseerr_config_path, 'db', 'db.sqlite3') 10 | jellyseerr_db_path = os.path.join(jellyseerr_config_path, 'db', 'db.sqlite3') 11 | 12 | overseerr_json = os.path.join(overseerr_config_path, 'settings.json') 13 | jellyseerr_json = os.path.join(jellyseerr_config_path, 'settings.json') 14 | 15 | tables = ['user', 'media', 'media_request', 'season', 'season_request', 'issue', 'issue_comment', 'user_push_subscription', 'user_settings'] 16 | 17 | def backup_configs(): 18 | backup_folder = './backup' 19 | os.makedirs(backup_folder, exist_ok=True) 20 | 21 | overseerr_backup_path = os.path.join(backup_folder, 'overseerr_backup') 22 | jellyseerr_backup_path = os.path.join(backup_folder, 'jellyseerr_backup') 23 | 24 | if os.path.exists(overseerr_config_path): 25 | shutil.copytree(overseerr_config_path, overseerr_backup_path, dirs_exist_ok=True) 26 | 27 | if os.path.exists(jellyseerr_config_path): 28 | shutil.copytree(jellyseerr_config_path, jellyseerr_backup_path, dirs_exist_ok=True) 29 | print("Backup complete.") 30 | 31 | backup_configs() 32 | 33 | # Migrate settings from Overseerr to Jellyseerr. 34 | def recursive_merge(source, dest, exclude_keys=None): 35 | """ 36 | Recursively update values in dest with those from source if the key exists in dest. 37 | For nested dictionaries, subkeys from source are added/updated in dest. 38 | """ 39 | if exclude_keys is None: 40 | exclude_keys = [] 41 | 42 | for key, value in source.items(): 43 | if key in exclude_keys: 44 | continue 45 | if key in dest: 46 | if isinstance(value, dict) and isinstance(dest[key], dict): 47 | dest[key] = recursive_merge(value, dest[key], exclude_keys) 48 | else: 49 | dest[key] = value 50 | else: 51 | dest[key] = value 52 | return dest 53 | with open(overseerr_json, 'r', encoding='utf-8') as overseerr_file: 54 | overseerr_settings = json.load(overseerr_file) 55 | 56 | with open(jellyseerr_json, 'r', encoding='utf-8') as jellyseerr_file: 57 | jellyseerr_settings = json.load(jellyseerr_file) 58 | 59 | exclude_keys = ['applicationTitle', 'applicationUrl'] 60 | 61 | updated_settings = recursive_merge(overseerr_settings, jellyseerr_settings, exclude_keys) 62 | 63 | # Set Jellyseerr media server type to Plex. 64 | updated_settings['main']['mediaServerType'] = 1 65 | 66 | with open(jellyseerr_json, 'w', encoding='utf-8') as jellyseerr_file: 67 | json.dump(updated_settings, jellyseerr_file, indent=2) 68 | 69 | print("Settings migration complete.") 70 | 71 | # Migrate tables from Overseerr to Jellyseerr. 72 | with sqlite3.connect(overseerr_db_path) as src_conn, sqlite3.connect(jellyseerr_db_path) as dst_conn: 73 | src_cursor = src_conn.cursor() 74 | dst_cursor = dst_conn.cursor() 75 | 76 | dst_cursor.execute('PRAGMA foreign_keys = OFF') 77 | 78 | for table in tables: 79 | print(f'Migrating table: {table}') 80 | 81 | dst_cursor.execute(f'DELETE FROM {table}') 82 | dst_conn.commit() 83 | 84 | src_cursor.execute(f'SELECT * FROM {table}') 85 | rows = src_cursor.fetchall() 86 | 87 | if not rows: 88 | print(f'No data in table: {table}') 89 | continue 90 | 91 | column_names = [description[0] for description in src_cursor.description] 92 | 93 | if table == 'user_settings': 94 | # Handle column mismatches for user_settings 95 | dst_cursor.execute("PRAGMA table_info(user_settings)") 96 | dest_cols_info = dst_cursor.fetchall() 97 | dest_columns = [info[1] for info in dest_cols_info] 98 | common_columns = [col for col in dest_columns if col in column_names] 99 | placeholders = ', '.join(['?'] * len(common_columns)) 100 | columns_list = ', '.join(common_columns) 101 | new_rows = [] 102 | for row in rows: 103 | row_dict = dict(zip(column_names, row)) 104 | new_row = tuple(row_dict[col] for col in common_columns) 105 | new_rows.append(new_row) 106 | insert_query = f'INSERT INTO {table} ({columns_list}) VALUES ({placeholders})' 107 | dst_cursor.executemany(insert_query, new_rows) 108 | dst_conn.commit() 109 | print(f'Migrated {len(new_rows)} data records in table {table}.') 110 | else: 111 | placeholders = ', '.join(['?'] * len(column_names)) 112 | columns = ', '.join(column_names) 113 | insert_query = f'INSERT INTO {table} ({columns}) VALUES ({placeholders})' 114 | dst_cursor.executemany(insert_query, rows) 115 | dst_conn.commit() 116 | print(f'Migrated {len(rows)} data records in table {table}.') 117 | 118 | dst_cursor.execute('PRAGMA foreign_keys = ON') 119 | 120 | print('Database migration complete.') 121 | -------------------------------------------------------------------------------- /jellyseerr/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pixelhunterX/seerr-database-migration/69218fed23ee47b86cf3843d1f4fb10dd62df8f2/jellyseerr/.gitkeep -------------------------------------------------------------------------------- /overseerr/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pixelhunterX/seerr-database-migration/69218fed23ee47b86cf3843d1f4fb10dd62df8f2/overseerr/.gitkeep --------------------------------------------------------------------------------