memeseeks 0.3.0__py3-none-any.whl
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.
- memeseeks/__init__.py +3 -0
- memeseeks/__main__.py +3 -0
- memeseeks/albums.py +161 -0
- memeseeks/browser/memeseeks.user.js +658 -0
- memeseeks/cli.py +374 -0
- memeseeks/evalkit.py +134 -0
- memeseeks/images.py +77 -0
- memeseeks/inbox.py +118 -0
- memeseeks/index.py +254 -0
- memeseeks/indexer.py +129 -0
- memeseeks/library.py +176 -0
- memeseeks/maintext.py +197 -0
- memeseeks/masking.py +16 -0
- memeseeks/models/__init__.py +10 -0
- memeseeks/models/clip.py +39 -0
- memeseeks/models/ocr.py +35 -0
- memeseeks/models/textembed.py +19 -0
- memeseeks/models/vlm.py +68 -0
- memeseeks/online.py +51 -0
- memeseeks/phone.py +152 -0
- memeseeks/remote.py +131 -0
- memeseeks/retrieval.py +12 -0
- memeseeks/review.py +131 -0
- memeseeks/search.py +144 -0
- memeseeks/server.py +425 -0
- memeseeks/service.py +335 -0
- memeseeks/settings.py +60 -0
- memeseeks/tidy.py +187 -0
- memeseeks/warmup.py +92 -0
- memeseeks/web/app.js +1122 -0
- memeseeks/web/cat.js +86 -0
- memeseeks/web/fonts/JetBrainsMono-OFL.txt +93 -0
- memeseeks/web/fonts/NotoSerifSC-OFL.txt +94 -0
- memeseeks/web/fonts/memeseeks-mono.woff2 +0 -0
- memeseeks/web/fonts/memeseeks-serif.woff2 +0 -0
- memeseeks/web/fonts/serif-chars.txt +1 -0
- memeseeks/web/frame.js +81 -0
- memeseeks/web/icons/apple-touch-icon.png +0 -0
- memeseeks/web/icons/icon-192.png +0 -0
- memeseeks/web/icons/icon-512.png +0 -0
- memeseeks/web/icons/icon-maskable-512.png +0 -0
- memeseeks/web/icons/logo.svg +1 -0
- memeseeks/web/icons/memeseeks.ico +0 -0
- memeseeks/web/index.html +80 -0
- memeseeks/web/manifest.webmanifest +36 -0
- memeseeks/web/popcat.js +4 -0
- memeseeks/web/style.css +364 -0
- memeseeks/web/sw.js +27 -0
- memeseeks/web/tokens.css +81 -0
- memeseeks-0.3.0.dist-info/METADATA +217 -0
- memeseeks-0.3.0.dist-info/RECORD +55 -0
- memeseeks-0.3.0.dist-info/WHEEL +5 -0
- memeseeks-0.3.0.dist-info/entry_points.txt +2 -0
- memeseeks-0.3.0.dist-info/licenses/LICENSE +21 -0
- memeseeks-0.3.0.dist-info/top_level.txt +1 -0
memeseeks/__init__.py
ADDED
memeseeks/__main__.py
ADDED
memeseeks/albums.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""图集 (like playlists), 我喜欢, and the memes removed from the library. One JSON file each, in the library.
|
|
2
|
+
|
|
3
|
+
A 图集 holds image ids in the order they were added, never copies: a meme in five 图集 is stored once,
|
|
4
|
+
and deleting a 图集 never deletes a meme. 我喜欢 (id "liked") always exists and cannot be deleted.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import secrets
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from .index import _atomic_write_text
|
|
16
|
+
|
|
17
|
+
FILE = "collections.json"
|
|
18
|
+
REMOVED_FILE = "removed.json"
|
|
19
|
+
LIKED = "liked"
|
|
20
|
+
VERSION = 1
|
|
21
|
+
MAX_NAME = 40
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CollectionError(ValueError):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _read(path: Path, default):
|
|
29
|
+
try:
|
|
30
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
31
|
+
except FileNotFoundError:
|
|
32
|
+
return default
|
|
33
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
34
|
+
raise CollectionError(f"{path.name} is damaged ({exc}); restore it from a backup or delete it") from exc
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Collections:
|
|
38
|
+
def __init__(self, library_root, clock=time.time):
|
|
39
|
+
self.path = Path(library_root) / FILE
|
|
40
|
+
self.clock = clock
|
|
41
|
+
self._lock = threading.Lock()
|
|
42
|
+
|
|
43
|
+
# ---------- storage ----------
|
|
44
|
+
|
|
45
|
+
def _load(self) -> dict:
|
|
46
|
+
data = _read(self.path, {"version": VERSION, "collections": []})
|
|
47
|
+
if not any(c["id"] == LIKED for c in data["collections"]):
|
|
48
|
+
data["collections"].insert(0, {"id": LIKED, "name": "我喜欢", "created": 0, "items": []})
|
|
49
|
+
return data
|
|
50
|
+
|
|
51
|
+
def _save(self, data: dict) -> None:
|
|
52
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
_atomic_write_text(self.path, json.dumps(data, ensure_ascii=False, indent=1))
|
|
54
|
+
|
|
55
|
+
def _find(self, data: dict, cid: str) -> dict:
|
|
56
|
+
for c in data["collections"]:
|
|
57
|
+
if c["id"] == cid:
|
|
58
|
+
return c
|
|
59
|
+
raise CollectionError("no such 图集")
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def _clean_name(name) -> str:
|
|
63
|
+
name = " ".join(str(name or "").split())
|
|
64
|
+
if not name:
|
|
65
|
+
raise CollectionError("a 图集 needs a name")
|
|
66
|
+
if len(name) > MAX_NAME:
|
|
67
|
+
raise CollectionError(f"names are at most {MAX_NAME} characters")
|
|
68
|
+
return name
|
|
69
|
+
|
|
70
|
+
# ---------- reading ----------
|
|
71
|
+
|
|
72
|
+
def all(self) -> list[dict]:
|
|
73
|
+
return self._load()["collections"]
|
|
74
|
+
|
|
75
|
+
def get(self, cid: str) -> dict:
|
|
76
|
+
return self._find(self._load(), cid)
|
|
77
|
+
|
|
78
|
+
def containing(self, image_id: str) -> list[str]:
|
|
79
|
+
return [c["id"] for c in self.all() if any(it["id"] == image_id for it in c["items"])]
|
|
80
|
+
|
|
81
|
+
# ---------- changing ----------
|
|
82
|
+
|
|
83
|
+
def create(self, name) -> dict:
|
|
84
|
+
name = self._clean_name(name)
|
|
85
|
+
with self._lock:
|
|
86
|
+
data = self._load()
|
|
87
|
+
if any(c["name"] == name for c in data["collections"]):
|
|
88
|
+
raise CollectionError(f"there is already a 图集 called {name}")
|
|
89
|
+
c = {"id": secrets.token_hex(4), "name": name, "created": self.clock(), "items": []}
|
|
90
|
+
data["collections"].append(c)
|
|
91
|
+
self._save(data)
|
|
92
|
+
return c
|
|
93
|
+
|
|
94
|
+
def rename(self, cid: str, name) -> dict:
|
|
95
|
+
name = self._clean_name(name)
|
|
96
|
+
with self._lock:
|
|
97
|
+
data = self._load()
|
|
98
|
+
c = self._find(data, cid)
|
|
99
|
+
if cid == LIKED:
|
|
100
|
+
raise CollectionError("我喜欢 cannot be renamed")
|
|
101
|
+
if any(o["name"] == name and o["id"] != cid for o in data["collections"]):
|
|
102
|
+
raise CollectionError(f"there is already a 图集 called {name}")
|
|
103
|
+
c["name"] = name
|
|
104
|
+
self._save(data)
|
|
105
|
+
return c
|
|
106
|
+
|
|
107
|
+
def delete(self, cid: str) -> None:
|
|
108
|
+
if cid == LIKED:
|
|
109
|
+
raise CollectionError("我喜欢 cannot be deleted")
|
|
110
|
+
with self._lock:
|
|
111
|
+
data = self._load()
|
|
112
|
+
data["collections"].remove(self._find(data, cid))
|
|
113
|
+
self._save(data)
|
|
114
|
+
|
|
115
|
+
def add(self, cid: str, image_ids: list[str]) -> int:
|
|
116
|
+
"""Append memes not in the 图集 yet; returns how many were added."""
|
|
117
|
+
with self._lock:
|
|
118
|
+
data = self._load()
|
|
119
|
+
c = self._find(data, cid)
|
|
120
|
+
have = {it["id"] for it in c["items"]}
|
|
121
|
+
now = self.clock()
|
|
122
|
+
new = [i for i in dict.fromkeys(image_ids) if i not in have]
|
|
123
|
+
c["items"] += [{"id": i, "added": now} for i in new]
|
|
124
|
+
self._save(data)
|
|
125
|
+
return len(new)
|
|
126
|
+
|
|
127
|
+
def remove(self, cid: str, image_ids: list[str]) -> int:
|
|
128
|
+
drop = set(image_ids)
|
|
129
|
+
with self._lock:
|
|
130
|
+
data = self._load()
|
|
131
|
+
c = self._find(data, cid)
|
|
132
|
+
before = len(c["items"])
|
|
133
|
+
c["items"] = [it for it in c["items"] if it["id"] not in drop]
|
|
134
|
+
self._save(data)
|
|
135
|
+
return before - len(c["items"])
|
|
136
|
+
|
|
137
|
+
def forget(self, image_ids: list[str]) -> None:
|
|
138
|
+
"""A meme left the library: take it out of every 图集."""
|
|
139
|
+
drop = set(image_ids)
|
|
140
|
+
with self._lock:
|
|
141
|
+
data = self._load()
|
|
142
|
+
for c in data["collections"]:
|
|
143
|
+
c["items"] = [it for it in c["items"] if it["id"] not in drop]
|
|
144
|
+
self._save(data)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class Removed:
|
|
148
|
+
"""Memes removed from the library that live in your own folders: hidden, never deleted from disk."""
|
|
149
|
+
|
|
150
|
+
def __init__(self, library_root):
|
|
151
|
+
self.path = Path(library_root) / REMOVED_FILE
|
|
152
|
+
self._lock = threading.Lock()
|
|
153
|
+
|
|
154
|
+
def ids(self) -> set[str]:
|
|
155
|
+
return set(_read(self.path, {"version": VERSION, "ids": []})["ids"])
|
|
156
|
+
|
|
157
|
+
def add(self, image_ids: list[str]) -> None:
|
|
158
|
+
with self._lock:
|
|
159
|
+
ids = self.ids() | set(image_ids)
|
|
160
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
161
|
+
_atomic_write_text(self.path, json.dumps({"version": VERSION, "ids": sorted(ids)}))
|