diff --git a/toot/api.py b/toot/api.py index 29811da..984ad37 100644 --- a/toot/api.py +++ b/toot/api.py @@ -1,11 +1,13 @@ +import mimetypes +from os import path import re import uuid -from typing import List +from typing import BinaryIO, List, Optional from urllib.parse import urlparse, urlencode, quote -from toot import http, CLIENT_NAME, CLIENT_WEBSITE -from toot.exceptions import AuthenticationError +from toot import App, User, http, CLIENT_NAME, CLIENT_WEBSITE +from toot.exceptions import AuthenticationError, ConsoleError from toot.utils import drop_empty_values, str_bool, str_bool_nullable SCOPES = 'read write follow' @@ -347,11 +349,40 @@ def anon_tag_timeline_generator(instance, hashtag, local=False, limit=20): return _anon_timeline_generator(instance, path, params) -def upload_media(app, user, file, description=None): - return http.post(app, user, '/api/v2/media', - data={'description': description}, - files={'file': file} - ).json() +def upload_media( + app: App, + user: User, + media: BinaryIO, + description: Optional[str] = None, + thumbnail: Optional[BinaryIO] = None, +): + data = drop_empty_values({"description": description}) + + # NB: Documentation says that "file" should provide a mime-type which we + # don't do currently, but it works. + files = drop_empty_values({ + "file": media, + "thumbnail": _add_mime_type(thumbnail) + }) + + return http.post(app, user, "/api/v2/media", data=data, files=files).json() + + +def _add_mime_type(file): + if file is None: + return None + + # TODO: mimetypes uses the file extension to guess the mime type which is + # not always good enough (e.g. files without extension). python-magic could + # be used instead but it requires adding it as a dependency. + mime_type = mimetypes.guess_type(file.name) + + if not mime_type: + raise ConsoleError(f"Unable guess mime type of '{file.name}'. " + "Ensure the file has the desired extension.") + + filename = path.basename(file.name) + return (filename, file, mime_type) def search(app, user, query, resolve=False, type=None): diff --git a/toot/commands.py b/toot/commands.py index 9941e23..1a887de 100644 --- a/toot/commands.py +++ b/toot/commands.py @@ -142,14 +142,16 @@ def _get_scheduled_at(scheduled_at, scheduled_in): def _upload_media(app, user, args): - # Match media to corresponding description and upload + # Match media to corresponding description and thumbnail media = args.media or [] descriptions = args.description or [] + thumbnails = args.thumbnail or [] uploaded_media = [] for idx, file in enumerate(media): description = descriptions[idx].strip() if idx < len(descriptions) else None - result = _do_upload(app, user, file, description) + thumbnail = thumbnails[idx] if idx < len(thumbnails) else None + result = _do_upload(app, user, file, description, thumbnail) uploaded_media.append(result) return [m["id"] for m in uploaded_media] @@ -297,7 +299,7 @@ def activate(app, user, args): def upload(app, user, args): - response = _do_upload(app, user, args.file, args.description) + response = _do_upload(app, user, args.file, args.description, None) msg = "Successfully uploaded media ID {}, type '{}'" @@ -312,9 +314,9 @@ def search(app, user, args): print_search_results(response) -def _do_upload(app, user, file, description): +def _do_upload(app, user, file, description, thumbnail): print_out("Uploading media: {}".format(file.name)) - return api.upload_media(app, user, file, description=description) + return api.upload_media(app, user, file, description=description, thumbnail=thumbnail) def _find_account(app, user, account_name): diff --git a/toot/console.py b/toot/console.py index f1f6994..ae98c6c 100644 --- a/toot/console.py +++ b/toot/console.py @@ -482,6 +482,12 @@ POST_COMMANDS = [ "help": "plain-text description of the media for accessibility " "purposes, one per attached media" }), + (["--thumbnail"], { + "action": "append", + "type": FileType("rb"), + "help": "path to an image file to serve as media thumbnail, " + "one per attached media" + }), visibility_arg, (["-s", "--sensitive"], { "action": 'store_true',