up2fireshare 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,51 @@
1
+ Metadata-Version: 2.4
2
+ Name: up2fireshare
3
+ Version: 1.0.0
4
+ Summary: Add your description here
5
+ Author: LittleBit
6
+ Author-email: LittleBit <littlebit@littlebitstudios.com>
7
+ License-Expression: MIT
8
+ Requires-Dist: argparse>=1.4.0
9
+ Requires-Dist: pyyaml>=6.0.3
10
+ Requires-Dist: requests>=2.34.2
11
+ Requires-Dist: xxhash>=4.0.1
12
+ Requires-Python: >=3.13
13
+ Description-Content-Type: text/markdown
14
+
15
+ # up2fireshare
16
+
17
+ A CLI for uploading game clips to the Fireshare self-hosted gaming media collection system.
18
+
19
+ ## Installation
20
+
21
+ up2fireshare is a Python package uploaded to PyPI so it can be installed with pip or pipx (preferably pipx).
22
+
23
+ ```sh
24
+ pipx install up2fireshare
25
+ ```
26
+
27
+ Most Linux distros provide pipx for running Python-based CLI tools, it can also be installed as `pip install pipx` on Windows or in cases where there is no distro package for pipx (for MacOS with `brew` (https://brew.sh), install with `brew install pipx`).
28
+
29
+ ## Usage
30
+
31
+ On first use you'll need to copy the `example-config.yaml` file to your PC as `[HOME FOLDER]/.littlebitstudios/up2fireshare/config.yaml`.
32
+
33
+ Your home folder should be `/home/username` on Linux, `/Users/username` on MacOS, or `C:/Users/Your Name` on Windows. If Windows was set up with a Microsoft Account, the home folder will be `C:/Users/[first 5 letters of email]`.
34
+
35
+ The most basic way to run the program is
36
+ ```sh
37
+ up2fireshare /path/to/media/file
38
+ ```
39
+ You can also use `-g` to specify a game name (this game must be one you've added already from Fireshare's interface), the name is case-insensitive. You can also use `-f` to specify the folder name within Fireshare to upload to.
40
+
41
+ If you want to use Fireshare's tags, you can use `-t "tag1, tag2, ..."` to upload with tags.
42
+
43
+ To upload multiple files at once, just add more than one file. For an entire folder, use `/path/to/media/folder/*`. Do note that when uploading multiple files, you can't use a title (`-n`) and all other options are copied across the files. Useful if you're uploading an entire folder that's categorized by game.
44
+
45
+ The program's full helptext can be seen with `up2fireshare -h`.
46
+
47
+ ## Licensing/Credits
48
+
49
+ This tool is open-source under the MIT license. See the LICENSE file.
50
+
51
+ This tool interacts with Fireshare (https://github.com/shaneisrael/fireshare), open-source software under the GPL-3.0 license.
@@ -0,0 +1,37 @@
1
+ # up2fireshare
2
+
3
+ A CLI for uploading game clips to the Fireshare self-hosted gaming media collection system.
4
+
5
+ ## Installation
6
+
7
+ up2fireshare is a Python package uploaded to PyPI so it can be installed with pip or pipx (preferably pipx).
8
+
9
+ ```sh
10
+ pipx install up2fireshare
11
+ ```
12
+
13
+ Most Linux distros provide pipx for running Python-based CLI tools, it can also be installed as `pip install pipx` on Windows or in cases where there is no distro package for pipx (for MacOS with `brew` (https://brew.sh), install with `brew install pipx`).
14
+
15
+ ## Usage
16
+
17
+ On first use you'll need to copy the `example-config.yaml` file to your PC as `[HOME FOLDER]/.littlebitstudios/up2fireshare/config.yaml`.
18
+
19
+ Your home folder should be `/home/username` on Linux, `/Users/username` on MacOS, or `C:/Users/Your Name` on Windows. If Windows was set up with a Microsoft Account, the home folder will be `C:/Users/[first 5 letters of email]`.
20
+
21
+ The most basic way to run the program is
22
+ ```sh
23
+ up2fireshare /path/to/media/file
24
+ ```
25
+ You can also use `-g` to specify a game name (this game must be one you've added already from Fireshare's interface), the name is case-insensitive. You can also use `-f` to specify the folder name within Fireshare to upload to.
26
+
27
+ If you want to use Fireshare's tags, you can use `-t "tag1, tag2, ..."` to upload with tags.
28
+
29
+ To upload multiple files at once, just add more than one file. For an entire folder, use `/path/to/media/folder/*`. Do note that when uploading multiple files, you can't use a title (`-n`) and all other options are copied across the files. Useful if you're uploading an entire folder that's categorized by game.
30
+
31
+ The program's full helptext can be seen with `up2fireshare -h`.
32
+
33
+ ## Licensing/Credits
34
+
35
+ This tool is open-source under the MIT license. See the LICENSE file.
36
+
37
+ This tool interacts with Fireshare (https://github.com/shaneisrael/fireshare), open-source software under the GPL-3.0 license.
@@ -0,0 +1,24 @@
1
+ [project]
2
+ name = "up2fireshare"
3
+ license = "MIT"
4
+ version = "1.0.0"
5
+ description = "Add your description here"
6
+ readme = "README.md"
7
+ requires-python = ">=3.13"
8
+ dependencies = [
9
+ "argparse>=1.4.0",
10
+ "pyyaml>=6.0.3",
11
+ "requests>=2.34.2",
12
+ "xxhash>=4.0.1",
13
+ ]
14
+
15
+ [[project.authors]]
16
+ name = "LittleBit"
17
+ email = "littlebit@littlebitstudios.com"
18
+
19
+ [project.scripts]
20
+ up2fireshare = "up2fireshare:main"
21
+
22
+ [build-system]
23
+ requires = ["uv_build>=0.13.0,<0.14.0"]
24
+ build-backend = "uv_build"
@@ -0,0 +1,23 @@
1
+ [project]
2
+ name = "up2fireshare"
3
+ license = "MIT"
4
+ version = "1.0.0"
5
+ description = "Add your description here"
6
+ readme = "README.md"
7
+ authors = [
8
+ { name = "LittleBit", email = "littlebit@littlebitstudios.com" }
9
+ ]
10
+ requires-python = ">=3.13"
11
+ dependencies = [
12
+ "argparse>=1.4.0",
13
+ "pyyaml>=6.0.3",
14
+ "requests>=2.34.2",
15
+ "xxhash>=4.0.1",
16
+ ]
17
+
18
+ [project.scripts]
19
+ up2fireshare = "up2fireshare:main"
20
+
21
+ [build-system]
22
+ requires = ["uv_build>=0.13.0,<0.14.0"]
23
+ build-backend = "uv_build"
@@ -0,0 +1,196 @@
1
+ import argparse
2
+ import os
3
+ from pathlib import Path
4
+ import requests
5
+ import xxhash
6
+ import yaml
7
+
8
+
9
+ _VIDEO_EXTENSIONS = {".mp4", ".m4v", ".mov", ".webm"}
10
+ _IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".gif"}
11
+ _HASH_BYTES = 16 * 1024 * 1024
12
+
13
+
14
+ def _default_config_path():
15
+ return Path.home() / ".littlebitstudios" / "up2fireshare" / "config.yaml"
16
+
17
+
18
+ def load_config(config_path=None):
19
+ config_file = Path(config_path).expanduser() if config_path else _default_config_path()
20
+
21
+ if not config_file.exists():
22
+ raise FileNotFoundError(f"Copy the example-config.yaml file from the repository to {config_file.parent}.\nRename the file to config.yaml and fill in the values for your Fireshare server.\nMake sure you specified --config if not using a config file in the default location.")
23
+
24
+ with config_file.open("r", encoding="utf-8") as handle:
25
+ config = yaml.safe_load(handle) or {}
26
+
27
+ if not isinstance(config, dict):
28
+ raise ValueError("The config file must contain a YAML mapping.")
29
+
30
+ server = config.get("server")
31
+ token = config.get("token")
32
+ if not server or not token:
33
+ raise KeyError("The config file must contain both 'server' and 'token'.")
34
+
35
+ return {"server": str(server).rstrip("/"), "token": str(token)}
36
+
37
+
38
+ def _media_id(file_path):
39
+ extension = Path(file_path).suffix.lower()
40
+ if extension in _VIDEO_EXTENSIONS:
41
+ field = "video_id"
42
+ elif extension in _IMAGE_EXTENSIONS:
43
+ field = "image_id"
44
+ else:
45
+ return None
46
+
47
+ hasher = xxhash.xxh3_128()
48
+ with open(file_path, "rb") as handle:
49
+ hasher.update(handle.read(_HASH_BYTES))
50
+ return field, hasher.hexdigest()
51
+
52
+
53
+ def upload_file(file_path, server, token, *, title=None, folder=None, game=None, tags=None):
54
+ file_name = Path(file_path).name
55
+ server_url = str(server).rstrip("/")
56
+ upload_url = f"{server_url}/api/upload/token"
57
+ payload = {}
58
+
59
+ if title is not None:
60
+ payload["title"] = str(title)
61
+ if folder is not None:
62
+ payload["folder"] = str(folder)
63
+ if game is not None:
64
+ payload["game"] = str(game)
65
+ if tags is not None:
66
+ payload["tag_ids"] = str(tags).strip()
67
+
68
+ media_id = _media_id(file_path)
69
+ if media_id is not None:
70
+ id_field, file_id = media_id
71
+ exists_response = requests.get(
72
+ f"{server_url}/api/upload/token/exists",
73
+ params={id_field: file_id},
74
+ headers={"Authorization": f"Bearer {token}"},
75
+ timeout=60,
76
+ )
77
+ exists_response.raise_for_status()
78
+ exists_result = exists_response.json()
79
+ if exists_result.get("exists") is True:
80
+ return exists_result
81
+
82
+ with open(file_path, "rb") as handle:
83
+ response = requests.post(
84
+ upload_url,
85
+ files={"file": (file_name, handle, "application/octet-stream")},
86
+ data=payload,
87
+ headers={"Authorization": f"Bearer {token}"},
88
+ timeout=60,
89
+ )
90
+
91
+ try:
92
+ return response.json()
93
+ except ValueError:
94
+ return {"status_code": response.status_code, "text": response.text}
95
+
96
+ def get_user_data(server, token):
97
+ response = requests.get(f"{server}/api/upload/token", headers={"Authorization": f"Bearer {token}"})
98
+
99
+ try:
100
+ return response.json()
101
+ except ValueError:
102
+ return {"status_code": response.status_code, "text": response.text}
103
+
104
+
105
+ def main():
106
+ parser = argparse.ArgumentParser(
107
+ description="Upload one or more files to a Fireshare instance."
108
+ )
109
+ parser.add_argument(
110
+ "files",
111
+ nargs="+",
112
+ help="File(s) to upload. Whether Fireshare treats them as video or photo depends on extension.",
113
+ )
114
+ parser.add_argument(
115
+ "--name",
116
+ "-n",
117
+ help="Title of the upload in Fireshare. Defaults to the filename (without extension).",
118
+ )
119
+ parser.add_argument(
120
+ "--tags",
121
+ "-t",
122
+ help="Tags to apply to uploaded clip(s), comma-separated.",
123
+ )
124
+ parser.add_argument(
125
+ "--folder",
126
+ "-f",
127
+ help="Folder on your Fireshare server to assign the clip to. Default: your server's upload folder.",
128
+ )
129
+ parser.add_argument(
130
+ "--game",
131
+ "-g",
132
+ help="Game name to assign to uploaded clip(s), case insensitive. Game must exist on your server already.",
133
+ )
134
+ parser.add_argument(
135
+ "--config",
136
+ help="Path to the YAML config file. Defaults to ~/.littlebitstudios/up2fireshare/config.yaml.",
137
+ )
138
+
139
+ args = parser.parse_args()
140
+ if args.name is not None and len(args.files) > 1:
141
+ parser.error("the title option (-n/--name) can only be used with a single file")
142
+
143
+ try:
144
+ config = load_config(args.config)
145
+ except FileNotFoundError as exc:
146
+ print(str(exc))
147
+ return 1
148
+ except (KeyError, ValueError) as exc:
149
+ print(f"Invalid config: {exc}")
150
+ return 1
151
+
152
+ user_data=get_user_data(config.get("server"), config.get("token"))
153
+ if user_data:
154
+ print(f"connected to {config.get("server")} as user {user_data.get("username")}\n")
155
+
156
+ normalized_tags = None
157
+ if args.tags:
158
+ normalized_tags = ",".join(part.strip() for part in args.tags.split(",") if part.strip())
159
+
160
+ for file_path in args.files:
161
+ file_path = os.path.expanduser(file_path)
162
+ if not os.path.exists(file_path):
163
+ print(f"File not found: {file_path}")
164
+ return 1
165
+
166
+ result = upload_file(
167
+ file_path,
168
+ config["server"],
169
+ config["token"],
170
+ title=args.name,
171
+ folder=args.folder,
172
+ game=args.game,
173
+ tags=normalized_tags,
174
+ )
175
+
176
+ if isinstance(result, dict):
177
+ status = (
178
+ "already exists"
179
+ if result.get("exists") is True
180
+ else result.get("status") or result.get("status_code")
181
+ )
182
+ print(f"{os.path.basename(file_path)} -> {status}")
183
+ if result.get("message"):
184
+ print(result["message"])
185
+ if result.get("error"):
186
+ print(result["error"])
187
+ if result.get("url"):
188
+ print(f"view item here: {config.get("server")}{result.get("url")}")
189
+ else:
190
+ print(result)
191
+
192
+ return 0
193
+
194
+
195
+ if __name__ == "__main__":
196
+ raise SystemExit(main())