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',