Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update remove-offline-files.md #8350

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 70 additions & 77 deletions docs/docs/guides/remove-offline-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,36 +19,36 @@ This page is a guide to get rid of offline files from the repair page.

This way works by retrieving a file that contains a list of all the files that are defined as offline files, running a script that uses the [Immich API](/docs/api/delete-assets) in order to remove the offline files.

1. Create an API key under Admin User -> Account Settings -> API Keys -> New API Key -> Copy to clipboard.
2. Copy and save the code to file -> `Immich Remove Offline Files.py`.
3. Run the script and follow the instructions.
1. Create an API key under Admin User -> Account Settings -> API Keys -> New API Key -> Make note of this for later.
2. Create an API key under the User that owns the Offline Files to be removed -> Account Settings -> API Keys -> New API Key -> Make note of this for later.
3. Copy and save the code to file -> `Immich Remove Offline Files.py`.
4. Run the script and follow the instructions.

:::note
You might need to run `pip install halo tabulate tqdm` if these dependencies are missing on your machine.
You might need to run `pip install requests halo tabulate tqdm` if these dependencies are missing on your machine.
:::

```bash title='Python'
#!/usr/bin/env python3

# Note: you might need to run "pip install halo tabulate tqdm" if these dependencies are missing on your machine
# Note: you might need to run "pip install requests halo tabulate tqdm" if these dependencies are missing on your machine

import argparse
import json
import requests

from datetime import datetime
from urllib.parse import urlparse
from halo import Halo
from tabulate import tabulate
from tqdm import tqdm
from urllib.parse import urlparse

def parse_arguments():
parser = argparse.ArgumentParser(description='Fetch file report and delete orphaned media assets from Immich.')
parser.add_argument('--apikey', help='Immich API key for authentication')
parser.add_argument('--immichaddress', help='Full address for Immich, including protocol and port')
# Make API key and Immich address arguments optional
parser.add_argument('--admin_apikey', help='Immich admin API key for fetching reports', nargs='?', default=None)
parser.add_argument('--user_apikey', help='User-specific Immich API key for deletion', nargs='?', default=None)
parser.add_argument('--immichaddress', help='Full address for Immich, including protocol and port', nargs='?', default=None)
parser.add_argument('--no_prompt', action='store_true', help='Delete orphaned media assets without confirmation')
args = parser.parse_args()
return args
return parser.parse_args()

def filter_entities(response_json, entity_type):
return [
Expand All @@ -58,78 +58,71 @@ def filter_entities(response_json, entity_type):

def main():
args = parse_arguments()
try:
if args.apikey:
api_key = args.apikey
else:
api_key = input('Enter the Immich API key: ')

if args.immichaddress:
immich_server = args.immichaddress
else:
immich_server = input('Enter the full web address for Immich, including protocol and port: ')
immich_parsed_url = urlparse(immich_server)
base_url = f'{immich_parsed_url.scheme}://{immich_parsed_url.netloc}'
api_url = f'{base_url}/api'
file_report_url = api_url + '/audit/file-report'
headers = {'x-api-key': api_key}

print()
spinner = Halo(text='Retrieving list of orphaned media assets...', spinner='dots')
spinner.start()
# Prompt for admin API key if not provided
admin_api_key = args.admin_apikey if args.admin_apikey else input('Enter the Immich admin API key: ')
# Prompt for user API key if not provided
user_api_key = args.user_apikey if args.user_apikey else input('Enter the Immich user API key for deletion: ')
# Prompt for Immich address if not provided
immich_server = args.immichaddress if args.immichaddress else input('Enter the full web address for Immich, including protocol and port: ')

try:
response = requests.get(file_report_url, headers=headers)
response.raise_for_status()
spinner.succeed('Success!')
except requests.exceptions.RequestException as e:
spinner.fail(f'Failed to fetch assets: {str(e)}')
if not admin_api_key or not user_api_key:
print("Both admin and user API keys are required.")
return

person_assets = filter_entities(response.json(), 'person')
orphan_media_assets = filter_entities(response.json(), 'asset')
immich_parsed_url = urlparse(immich_server)
base_url = f'{immich_parsed_url.scheme}://{immich_parsed_url.netloc}'
api_url = f'{base_url}/api'
file_report_url = api_url + '/audit/file-report'
headers = {'x-api-key': admin_api_key}

num_entries = len(orphan_media_assets)
print()
spinner = Halo(text='Retrieving list of orphaned media assets...', spinner='dots')
spinner.start()

if num_entries == 0:
print('No orphaned media assets found; exiting.')
return
try:
response = requests.get(file_report_url, headers=headers)
response.raise_for_status()
spinner.succeed('Success!')
except requests.exceptions.RequestException as e:
spinner.fail(f'Failed to fetch assets: {str(e)}')
return

orphan_media_assets = filter_entities(response.json(), 'asset')
num_entries = len(orphan_media_assets)

if num_entries == 0:
print('No orphaned media assets found; exiting.')
return

if not args.no_prompt:
table_data = [[asset['pathValue'], asset['entityId']] for asset in orphan_media_assets]
print(tabulate(table_data, headers=['Path Value', 'Entity ID'], tablefmt='pretty'))
print()

else:
if not args.no_prompt:
table_data = []
for asset in orphan_media_assets:
table_data.append([asset['pathValue'], asset['entityId']])
print(tabulate(table_data, headers=['Path Value', 'Entity ID'], tablefmt='pretty'))
print()

if person_assets:
print('Found orphaned person assets! Please run the "RECOGNIZE FACES > ALL" job in Immich after running this tool to correct this.')
print()

if num_entries > 0:
summary = f'There {"is" if num_entries == 1 else "are"} {num_entries} orphaned media asset{"s" if num_entries != 1 else ""}. Would you like to delete {"them" if num_entries != 1 else "it"} from Immich? (yes/no): '
user_input = input(summary).lower()
print()

if user_input not in ('y', 'yes'):
print('Exiting without making any changes.')
return

with tqdm(total=num_entries, desc="Deleting orphaned media assets", unit="asset") as progress_bar:
for asset in orphan_media_assets:
entity_id = asset['entityId']
asset_url = f'{api_url}/asset'
delete_payload = json.dumps({'force': True, 'ids': [entity_id]})
headers = {'Content-Type': 'application/json', 'x-api-key': api_key}
response = requests.delete(asset_url, headers=headers, data=delete_payload)
response.raise_for_status()
progress_bar.set_postfix_str(entity_id)
progress_bar.update(1)
print()
print('Orphaned media assets deleted successfully!')
except Exception as e:
summary = f'There {"is" if num_entries == 1 else "are"} {num_entries} orphaned media asset{"s" if num_entries != 1 else ""}. Would you like to delete {"them" if num_entries != 1 else "it"} from Immich? (yes/no): '
user_input = input(summary).lower()
print()
print(f"An error occurred: {str(e)}")

if user_input not in ('y', 'yes'):
print('Exiting without making any changes.')
return

headers['x-api-key'] = user_api_key # Use user API key for deletion
with tqdm(total=num_entries, desc="Deleting orphaned media assets", unit="asset") as progress_bar:
for asset in orphan_media_assets:
entity_id = asset['entityId']
asset_url = f'{api_url}/asset'
delete_payload = json.dumps({'force': True, 'ids': [entity_id]})
headers = {'Content-Type': 'application/json', 'x-api-key': user_api_key}
try:
response = requests.delete(asset_url, headers=headers, data=delete_payload)
response.raise_for_status()
except requests.exceptions.RequestException as e:
print(f"Failed to delete asset {entity_id}: {str(e)}")
continue
progress_bar.update(1)
print('Orphaned media assets deleted successfully!')

if __name__ == '__main__':
main()
Expand Down