WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit 4e805b0

Browse files
committed
Added support for moving play history as well
1 parent 5323c7c commit 4e805b0

File tree

1 file changed

+107
-75
lines changed

1 file changed

+107
-75
lines changed

transfer-plex-user-viewstate.py

Lines changed: 107 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ def get_valid_input(prompt: str, valid_values: list, invalid_msg: str) -> str:
99
input_value = None
1010
input_valid = False
1111
while not input_valid:
12-
input_value = input(f"{prompt} ")
12+
input_value = input(f'{prompt} ')
1313
input_valid = input_value in valid_values
1414
if not input_valid:
1515
print(invalid_msg)
@@ -22,8 +22,8 @@ def str_range(stop: int) -> list:
2222
return [str(v) for v in values]
2323

2424

25-
parser = argparse.ArgumentParser("Transfer Plex user viewstate between users")
26-
parser.add_argument("-p", "--db-path", help="Path to com.plexapp.plugins.library.db file")
25+
parser = argparse.ArgumentParser(description='Transfer Plex user viewstate and play history between users')
26+
parser.add_argument('-p', '--db-path', help='Path to com.plexapp.plugins.library.db file', type=str, required=True)
2727
args = parser.parse_args()
2828

2929
connection = sqlite3.connect(args.db_path)
@@ -32,113 +32,145 @@ def str_range(stop: int) -> list:
3232
cursor = connection.cursor()
3333

3434
# Get users
35-
cursor.execute("SELECT id, name, created_at FROM accounts WHERE id > 0")
35+
cursor.execute('SELECT id, name, created_at FROM accounts WHERE id > 0')
3636
accounts = cursor.fetchall()
3737

3838
# Print users
39-
print(tabulate([{"index": i, "name": accounts[i]["name"]} for i in range(0, len(accounts))], headers="keys"))
39+
print(tabulate([{'index': i, 'name': accounts[i]['name']} for i in range(0, len(accounts))], headers='keys'))
4040

4141
# Ask user to select source user
4242
sourceAccountIndex = int(get_valid_input(
43-
"First, please enter the _source_ account index",
43+
'First, please enter the _source_ account index',
4444
str_range(len(accounts)),
45-
"No account with that ID, please enter a valid account index"
45+
'No account with that ID, please enter a valid account index'
4646
))
4747

4848
# Ask user to select target user
4949
targetAccountIndex = int(get_valid_input(
50-
"Now, please enter the _target_ account index",
50+
'Now, please enter the _target_ account index',
5151
str_range(len(accounts)),
52-
"No account with that ID, please enter a valid account index"
52+
'No account with that ID, please enter a valid account index'
5353
))
5454

5555
# Ask user to select copying/moving view state
5656
mode = get_valid_input(
57-
"Do you want to copy or move the viewstate? ([c]opy/[m]move)",
58-
["c", "copy", "m", "move"],
59-
"Please select either [c]opy or [m]ove"
57+
'Do you want to copy or move the viewstate? ([c]opy/[m]move)',
58+
['c', 'copy', 'm', 'move'],
59+
'Please select either [c]opy or [m]ove'
6060
)
6161

6262
# Get source account viewstate
63-
sql = "SELECT * FROM metadata_item_settings WHERE account_id = :account_id"
64-
cursor.execute(sql, {"account_id": accounts[sourceAccountIndex]["id"]})
63+
sql = 'SELECT guid, rating, view_offset, view_count, last_viewed_at, created_at, updated_at, skip_count, ' \
64+
'last_skipped_at, changed_at, extra_data, last_rated_at FROM metadata_item_settings ' \
65+
'WHERE account_id = :account_id'
66+
cursor.execute(sql, {'account_id': accounts[sourceAccountIndex]['id']})
6567
sourceViewstate = cursor.fetchall()
6668

69+
# Get source account play history
70+
sql = 'SELECT guid, metadata_type, library_section_id, grandparent_title, parent_index, parent_title, ' \
71+
'"index", title, thumb_url, viewed_at, grandparent_guid, originally_available_at, device_id ' \
72+
'FROM metadata_item_views WHERE account_id = :account_id'
73+
cursor.execute(sql, {'account_id': accounts[sourceAccountIndex]['id']})
74+
sourcePlayHistory = cursor.fetchall()
75+
6776
# Make sure source account has any viewstate items
68-
if len(sourceViewstate) == 0:
69-
sys.exit("Source account has no viewstate items")
77+
if len(sourceViewstate) == 0 and len(sourcePlayHistory) == 0:
78+
sys.exit('Source account has no viewstate/play history items')
7079

7180
# Check for existing viewstate items of target user
72-
sql = "SELECT id, guid FROM metadata_item_settings WHERE account_id = :account_id"
73-
cursor.execute(sql, {"account_id": accounts[targetAccountIndex]['id']})
81+
sql = 'SELECT id, guid FROM metadata_item_settings WHERE account_id = :account_id'
82+
cursor.execute(sql, {'account_id': accounts[targetAccountIndex]['id']})
7483
targetViewstate = cursor.fetchall()
7584

85+
# Check for existing play history items of target user
86+
sql = 'SELECT id, guid FROM metadata_item_views WHERE account_id = :account_id'
87+
cursor.execute(sql, {'account_id': accounts[targetAccountIndex]['id']})
88+
targetPlayHistory = cursor.fetchall()
89+
7690
# Handle existing target account viewstates items if any were found
77-
if len(targetViewstate) > 0:
78-
print(f"Found {len(targetViewstate)} existing viewstate items for target account")
91+
if len(targetViewstate) > 0 or len(targetPlayHistory):
92+
print(f'Found existing viewstate ({len(targetViewstate)} items)/play history ({len(targetPlayHistory)} items) '
93+
f'for target account')
7994
handleExisting = get_valid_input(
80-
"Do you want to add to or replace existing viewstate? ([a]dd/[r]eplace)",
81-
["a", "add", "r", "replace"],
82-
"Please select either [a]dd/[r]eplace"
95+
'Do you want to add to or replace existing viewstate/play history? ([a]dd/[r]eplace)',
96+
['a', 'add', 'r', 'replace'],
97+
'Please select either [a]dd/[r]eplace'
8398
)
8499

85-
if handleExisting in ["a", "add"]:
100+
if handleExisting in ['a', 'add']:
86101
# Remove source account's viewstate for any media items for which target user has an existing viewstate
87-
sourceViewstateGuids = [e["guid"] for e in sourceViewstate]
88-
targetViewstateGuids = [e["guid"] for e in targetViewstate]
89-
for guid in targetViewstateGuids:
90-
# Check if guid is also present in source viewstate
91-
if guid in sourceViewstateGuids:
92-
# Get index of media item
93-
# (indexes are the same between guid list and overall source viewstate list,
94-
# so we can use the index to remove the media item's source viewstate from the copy/move list)
95-
index = sourceViewstateGuids.index(guid)
96-
# Remove viewstate for media item from copy/move list
97-
del sourceViewstate[index], sourceViewstateGuids[index]
98-
99-
# If we are moving the viewstate, remove viewstate items we don't need to move from source user
100-
sql = "DELETE FROM metadata_item_settings WHERE account_id = :account_id AND guid = :guid"
101-
cursor.execute(sql, {"account_id": accounts[sourceAccountIndex]["id"], "guid": guid})
102-
elif handleExisting in ["r", "replace"]:
102+
sourceViewstateGuids = [e['guid'] for e in sourceViewstate]
103+
viewstateGuidsToDelete = [e['guid'] for e in targetViewstate if e['guid'] in sourceViewstateGuids]
104+
for guid in viewstateGuidsToDelete:
105+
# Get index of media item
106+
# (indexes are the same between guid list and overall source viewstate list,
107+
# so we can use the index to remove the media item's source viewstate from the copy/move list)
108+
index = sourceViewstateGuids.index(guid)
109+
# Remove viewstate for media item from copy/move list
110+
del sourceViewstate[index], sourceViewstateGuids[index]
111+
112+
# If we are moving the viewstate, remove viewstate items we don't need to move from source user
113+
if mode in ['m', 'move']:
114+
sql = 'DELETE FROM metadata_item_settings WHERE account_id = :account_id AND guid = :guid'
115+
cursor.execute(sql, {'account_id': accounts[sourceAccountIndex]['id'], 'guid': guid})
116+
117+
# Remove source account's play history accordingly
118+
sourcePlayHistoryGuids = [e['guid'] for e in sourcePlayHistory]
119+
# Play history may contain multiple entries for one guid (one user playing an item multiple items)
120+
# => make sure to get a list of unique guids
121+
playHistoryGuidsToDelete = list(set([e['guid'] for e in targetPlayHistory
122+
if e['guid'] in sourcePlayHistoryGuids]))
123+
for guid in playHistoryGuidsToDelete:
124+
# Delete add play history items for guid from source user list (and database, if we are moving)
125+
while guid in sourcePlayHistoryGuids:
126+
index = sourcePlayHistoryGuids.index(guid)
127+
del sourcePlayHistory[index], sourcePlayHistoryGuids[index]
128+
if mode in ['m', 'move']:
129+
sql = 'DELETE FROM metadata_item_views WHERE account_id = :account_id AND guid = :guid'
130+
cursor.execute(sql, {'account_id': accounts[sourceAccountIndex]['id'], 'guid': guid})
131+
elif handleExisting in ['r', 'replace']:
103132
# Remove existing viewstate items
104133
for entry in targetViewstate:
105-
sql = "DELETE FROM metadata_item_settings WHERE id = :id"
106-
cursor.execute(sql, {"id": entry["id"]})
107-
108-
if mode in ["c", "copy"]:
109-
print(f"Copying viewstate ({len(sourceViewstate)} items) "
110-
f"from {accounts[sourceAccountIndex]['name']} to {accounts[targetAccountIndex]['name']}")
111-
112-
for entry in sourceViewstate:
113-
sql = "INSERT INTO metadata_item_settings (account_id, guid, rating, view_offset, view_count, last_viewed_at, " \
114-
"created_at, updated_at, skip_count, last_skipped_at, changed_at, extra_data, last_rated_at) VALUES (?, " \
115-
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
116-
cursor.execute(sql, (
117-
accounts[targetAccountIndex]["id"],
118-
entry["guid"],
119-
entry["rating"],
120-
entry["view_offset"],
121-
entry["view_count"],
122-
entry["last_viewed_at"],
123-
entry["created_at"],
124-
entry["updated_at"],
125-
entry["skip_count"],
126-
entry["last_skipped_at"],
127-
entry["changed_at"],
128-
entry["extra_data"],
129-
entry["last_rated_at"]
130-
))
134+
sql = 'DELETE FROM metadata_item_settings WHERE id = :id'
135+
cursor.execute(sql, {'id': entry['id']})
136+
137+
# Remove existing play history items
138+
for entry in targetPlayHistory:
139+
sql = 'DELETE FROM metadata_item_views WHERE id = :id'
140+
cursor.execute(sql, {'id': entry['id']})
141+
142+
if mode in ['c', 'copy']:
143+
print(f'Copying viewstate ({len(sourceViewstate)} items) and play history ({len(sourcePlayHistory)} items) '
144+
f'from {accounts[sourceAccountIndex]["name"]} to {accounts[targetAccountIndex]["name"]}')
145+
146+
# Copy viewstate items
147+
sql = 'INSERT INTO metadata_item_settings (account_id, guid, rating, view_offset, view_count, last_viewed_at, ' \
148+
'created_at, updated_at, skip_count, last_skipped_at, changed_at, extra_data, last_rated_at) VALUES (?, ' \
149+
'?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) '
150+
for existingEntry in sourceViewstate:
151+
newEntry = {'account_id': accounts[targetAccountIndex]['id'], **existingEntry}
152+
cursor.execute(sql, list(newEntry.values()))
153+
154+
# Copy play history items
155+
sql = 'INSERT INTO metadata_item_views (account_id, guid, metadata_type, library_section_id, grandparent_title, ' \
156+
'parent_index, parent_title, "index", title, thumb_url, viewed_at, grandparent_guid, ' \
157+
'originally_available_at, device_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
158+
for existingEntry in sourcePlayHistory:
159+
newEntry = {'account_id': accounts[targetAccountIndex]['id'], **existingEntry}
160+
cursor.execute(sql, list(newEntry.values()))
131161

132162
connection.commit()
133-
elif mode in ["m", "move"]:
134-
print(f"Moving viewstate ({len(sourceViewstate)} items) "
135-
f"from {accounts[sourceAccountIndex]['name']} to {accounts[targetAccountIndex]['name']}")
136-
137-
sql = "UPDATE metadata_item_settings SET account_id = :target_account_id WHERE account_id = :source_account_id"
138-
cursor.execute(sql, {
139-
"target_account_id": accounts[targetAccountIndex]["id"],
140-
"source_account_id": accounts[sourceAccountIndex]["id"]
141-
})
163+
elif mode in ['m', 'move']:
164+
print(f'Moving viewstate ({len(sourceViewstate)} items) and play history ({len(sourcePlayHistory)} items) '
165+
f'from {accounts[sourceAccountIndex]["name"]} to {accounts[targetAccountIndex]["name"]}')
166+
167+
tables = ['metadata_item_settings', 'metadata_item_views']
168+
for table in tables:
169+
sql = f'UPDATE {table} SET account_id = :target_account_id WHERE account_id = :source_account_id'
170+
cursor.execute(sql, {
171+
'target_account_id': accounts[targetAccountIndex]['id'],
172+
'source_account_id': accounts[sourceAccountIndex]['id']
173+
})
142174

143175
connection.commit()
144176

0 commit comments

Comments
 (0)