tt-help-cli-ycl 1.3.55 → 1.3.58
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.
- package/package.json +1 -1
- package/src/cli/open.js +3 -4
- package/src/lib/api-interceptor.js +6 -2
- package/src/scraper/explore-core.js +12 -0
- package/src/watch/data-store.js +457 -109
- package/src/watch/public/app.js +1266 -0
- package/src/watch/public/index.html +4 -2088
- package/src/watch/public/style.css +1070 -0
- package/src/watch/server.js +64 -3
- package/scripts/run-explore copy.bat +0 -101
- package/scripts/test-captcha-lib.mjs +0 -68
- package/scripts/test-captcha.mjs +0 -81
- package/scripts/test-html-analysis.mjs +0 -128
- package/scripts/test-incognito-lib.mjs +0 -36
- package/scripts/test-login-state.mjs +0 -128
- package/scripts/test-safe-click.mjs +0 -45
- package/scripts/test-watch-db-smoke.mjs +0 -246
package/src/watch/data-store.js
CHANGED
|
@@ -129,6 +129,9 @@ function initUserDb(filePath) {
|
|
|
129
129
|
if (!existingJobColumns.has("status_code")) {
|
|
130
130
|
db.exec(`ALTER TABLE jobs ADD COLUMN status_code INTEGER`);
|
|
131
131
|
}
|
|
132
|
+
if (!existingJobColumns.has("latest_video_time")) {
|
|
133
|
+
db.exec(`ALTER TABLE jobs ADD COLUMN latest_video_time INTEGER`);
|
|
134
|
+
}
|
|
132
135
|
db.exec(`
|
|
133
136
|
CREATE TABLE IF NOT EXISTS raw_jobs (
|
|
134
137
|
unique_id TEXT PRIMARY KEY,
|
|
@@ -159,7 +162,8 @@ function initUserDb(filePath) {
|
|
|
159
162
|
region TEXT,
|
|
160
163
|
signature TEXT,
|
|
161
164
|
sec_uid TEXT,
|
|
162
|
-
status_code INTEGER
|
|
165
|
+
status_code INTEGER,
|
|
166
|
+
latest_video_time INTEGER
|
|
163
167
|
)
|
|
164
168
|
`);
|
|
165
169
|
|
|
@@ -173,6 +177,9 @@ function initUserDb(filePath) {
|
|
|
173
177
|
if (!existingRawJobColumns.has("status_code")) {
|
|
174
178
|
db.exec(`ALTER TABLE raw_jobs ADD COLUMN status_code INTEGER`);
|
|
175
179
|
}
|
|
180
|
+
if (!existingRawJobColumns.has("latest_video_time")) {
|
|
181
|
+
db.exec(`ALTER TABLE raw_jobs ADD COLUMN latest_video_time INTEGER`);
|
|
182
|
+
}
|
|
176
183
|
db.exec(`
|
|
177
184
|
CREATE TABLE IF NOT EXISTS videos (
|
|
178
185
|
id TEXT PRIMARY KEY,
|
|
@@ -187,7 +194,8 @@ function initUserDb(filePath) {
|
|
|
187
194
|
comment_count INTEGER,
|
|
188
195
|
share_count INTEGER,
|
|
189
196
|
collect_count INTEGER,
|
|
190
|
-
stats_updated_at INTEGER
|
|
197
|
+
stats_updated_at INTEGER,
|
|
198
|
+
create_time INTEGER
|
|
191
199
|
)
|
|
192
200
|
`);
|
|
193
201
|
db.exec(`
|
|
@@ -280,6 +288,11 @@ function initUserDb(filePath) {
|
|
|
280
288
|
}
|
|
281
289
|
}
|
|
282
290
|
|
|
291
|
+
// 迁移:videos 表添加 create_time 列
|
|
292
|
+
if (!existingVideoColumns.has("create_time")) {
|
|
293
|
+
db.exec(`ALTER TABLE videos ADD COLUMN create_time INTEGER`);
|
|
294
|
+
}
|
|
295
|
+
|
|
283
296
|
const count = db.prepare("SELECT COUNT(*) as c FROM users").get().c;
|
|
284
297
|
console.log(`[data-store] SQLite users 表初始化完成: ${count} 条`);
|
|
285
298
|
}
|
|
@@ -311,9 +324,10 @@ export function importLegacyJsonToDb({
|
|
|
311
324
|
location_created,
|
|
312
325
|
tt_seller,
|
|
313
326
|
registered_at,
|
|
314
|
-
user_update_count
|
|
327
|
+
user_update_count,
|
|
328
|
+
create_time
|
|
315
329
|
)
|
|
316
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
330
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
317
331
|
`);
|
|
318
332
|
|
|
319
333
|
const importUsersTxn = db.transaction((items) => {
|
|
@@ -336,6 +350,7 @@ export function importLegacyJsonToDb({
|
|
|
336
350
|
item.ttSeller ? 1 : 0,
|
|
337
351
|
item.registeredAt || item.registered_at || Date.now(),
|
|
338
352
|
item.userUpdateCount || item.user_update_count || 0,
|
|
353
|
+
item.createTime || item.create_time || null,
|
|
339
354
|
);
|
|
340
355
|
}
|
|
341
356
|
});
|
|
@@ -857,7 +872,8 @@ function moveJobsToRawByCountry(scope, country) {
|
|
|
857
872
|
updated_at,
|
|
858
873
|
region,
|
|
859
874
|
signature,
|
|
860
|
-
sec_uid
|
|
875
|
+
sec_uid,
|
|
876
|
+
latest_video_time
|
|
861
877
|
)
|
|
862
878
|
SELECT
|
|
863
879
|
unique_id,
|
|
@@ -887,7 +903,8 @@ function moveJobsToRawByCountry(scope, country) {
|
|
|
887
903
|
updated_at,
|
|
888
904
|
region,
|
|
889
905
|
signature,
|
|
890
|
-
sec_uid
|
|
906
|
+
sec_uid,
|
|
907
|
+
latest_video_time
|
|
891
908
|
FROM jobs
|
|
892
909
|
WHERE ${whereSql}
|
|
893
910
|
`,
|
|
@@ -1168,6 +1185,17 @@ function getRawJobsPageFromDb({ search, location, limit, offset }) {
|
|
|
1168
1185
|
};
|
|
1169
1186
|
}
|
|
1170
1187
|
|
|
1188
|
+
// 调试接口:直接执行 SQL 查询,返回原始数据
|
|
1189
|
+
function rawQuery(sql, params = []) {
|
|
1190
|
+
if (!db) return { error: "db not ready" };
|
|
1191
|
+
try {
|
|
1192
|
+
const rows = db.prepare(sql).all(...params);
|
|
1193
|
+
return { rows };
|
|
1194
|
+
} catch (e) {
|
|
1195
|
+
return { error: e.message };
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1171
1199
|
function getUsersPageFromDb({
|
|
1172
1200
|
status,
|
|
1173
1201
|
search,
|
|
@@ -1282,6 +1310,7 @@ function getTargetUsersFromDb(targetLocations = []) {
|
|
|
1282
1310
|
tt_seller,
|
|
1283
1311
|
verified,
|
|
1284
1312
|
location_created,
|
|
1313
|
+
latest_video_time,
|
|
1285
1314
|
status,
|
|
1286
1315
|
sources
|
|
1287
1316
|
FROM jobs
|
|
@@ -1348,6 +1377,7 @@ const writableJobColumns = new Set([
|
|
|
1348
1377
|
"signature",
|
|
1349
1378
|
"sec_uid",
|
|
1350
1379
|
"status_code",
|
|
1380
|
+
"latest_video_time",
|
|
1351
1381
|
]);
|
|
1352
1382
|
|
|
1353
1383
|
function normalizeJobValue(column, value) {
|
|
@@ -1838,12 +1868,15 @@ export function createStore(filePath) {
|
|
|
1838
1868
|
const args = [];
|
|
1839
1869
|
if (!loggedIn) {
|
|
1840
1870
|
where.push("COALESCE(tt_seller, 0) != 1");
|
|
1841
|
-
//
|
|
1842
|
-
where.push("status_code IS NULL");
|
|
1871
|
+
// 未登录:只能领取 status_code 为空或 0 的任务
|
|
1872
|
+
where.push("(status_code IS NULL OR status_code = 0)");
|
|
1843
1873
|
} else {
|
|
1844
|
-
//
|
|
1845
|
-
where.push(
|
|
1874
|
+
// 登录:可以领取 status_code 为空、0、或 209002 的任务
|
|
1875
|
+
where.push(
|
|
1876
|
+
"(status_code IS NULL OR status_code = 0 OR status_code = 209002)",
|
|
1877
|
+
);
|
|
1846
1878
|
}
|
|
1879
|
+
// 其他 status_code 值的任务不被领取
|
|
1847
1880
|
if (requireVideo) {
|
|
1848
1881
|
where.push("COALESCE(video_count, 0) > 0");
|
|
1849
1882
|
}
|
|
@@ -2020,124 +2053,433 @@ export function createStore(filePath) {
|
|
|
2020
2053
|
return null;
|
|
2021
2054
|
}
|
|
2022
2055
|
|
|
2023
|
-
|
|
2056
|
+
if (!db) {
|
|
2057
|
+
const now = Date.now();
|
|
2024
2058
|
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2059
|
+
// 0. 该客户端有未过期的任务,续期返回
|
|
2060
|
+
const ongoing = data.find(
|
|
2061
|
+
(u) =>
|
|
2062
|
+
u.status === "processing" &&
|
|
2063
|
+
u.claimedBy === userId &&
|
|
2064
|
+
u.claimedAt &&
|
|
2065
|
+
now - u.claimedAt < expireMs,
|
|
2066
|
+
);
|
|
2067
|
+
if (ongoing) {
|
|
2068
|
+
ongoing.claimedAt = now;
|
|
2069
|
+
save();
|
|
2070
|
+
return {
|
|
2071
|
+
uniqueId: ongoing.uniqueId,
|
|
2072
|
+
nickname: ongoing.nickname,
|
|
2073
|
+
claimedAt: ongoing.claimedAt,
|
|
2074
|
+
claimedBy: userId,
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2043
2077
|
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2078
|
+
// 按猜测国家梯队排序
|
|
2079
|
+
const tier1 = new Set(["PL", "NL", "BE"]);
|
|
2080
|
+
const tier2 = new Set(["DE", "FR", "IT", "IE", "ES"]);
|
|
2081
|
+
function locationTier(u) {
|
|
2082
|
+
const loc = (u.guessedLocation || "").toUpperCase();
|
|
2083
|
+
if (tier1.has(loc)) return 0;
|
|
2084
|
+
if (tier2.has(loc)) return 1;
|
|
2085
|
+
return 2;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
// 国家过滤:如果指定了 locations,只保留 guessedLocation 在列表中的用户
|
|
2089
|
+
function locationFilter(u) {
|
|
2090
|
+
if (!locations || locations.length === 0) return true;
|
|
2091
|
+
return isLocationInList(u.guessedLocation, locations);
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
// 从候选列表中按优先级取第一个:pinned > 超时回收 > seed > ttSeller(仅登录) > follow > other
|
|
2095
|
+
function pickCandidate(candidates) {
|
|
2096
|
+
let next = candidates.find((u) => u.pinned);
|
|
2097
|
+
|
|
2098
|
+
if (!next) {
|
|
2099
|
+
const expired = data.find(
|
|
2100
|
+
(u) =>
|
|
2101
|
+
u.status === "processing" &&
|
|
2102
|
+
u.claimedAt &&
|
|
2103
|
+
now - u.claimedAt > expireMs,
|
|
2104
|
+
);
|
|
2105
|
+
if (expired) {
|
|
2106
|
+
expired.status = "pending";
|
|
2107
|
+
markStatsDirty();
|
|
2108
|
+
delete expired.claimedAt;
|
|
2109
|
+
next = expired;
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2053
2112
|
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2113
|
+
if (!next) {
|
|
2114
|
+
const seed = candidates.filter(
|
|
2115
|
+
(u) => u.sources && u.sources.includes("seed"),
|
|
2116
|
+
);
|
|
2117
|
+
seed.sort((a, b) => locationTier(a) - locationTier(b));
|
|
2118
|
+
next = seed[0] || null;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
// 未登录时跳过 ttSeller 优先级
|
|
2122
|
+
if (!next && loggedIn) {
|
|
2123
|
+
const ttSeller = candidates.filter(
|
|
2124
|
+
(u) => u.ttSeller === true && u.verified === false,
|
|
2125
|
+
);
|
|
2126
|
+
ttSeller.sort((a, b) => locationTier(a) - locationTier(b));
|
|
2127
|
+
next = ttSeller[0] || null;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
if (!next) {
|
|
2131
|
+
const follow = candidates.filter(
|
|
2132
|
+
(u) =>
|
|
2133
|
+
u.sources &&
|
|
2134
|
+
(u.sources.includes("following") ||
|
|
2135
|
+
u.sources.includes("follower")),
|
|
2136
|
+
);
|
|
2137
|
+
follow.sort((a, b) => locationTier(a) - locationTier(b));
|
|
2138
|
+
next = follow[0] || null;
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
if (!next) {
|
|
2142
|
+
candidates.sort((a, b) => locationTier(a) - locationTier(b));
|
|
2143
|
+
next = candidates[0] || null;
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
return next;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
// 先在有视频的 pending 用户中找;找不到再用全部 pending 用户兜底
|
|
2150
|
+
let pending = data.filter((u) => u.status === "pending");
|
|
2151
|
+
// 应用国家过滤
|
|
2152
|
+
if (locations && locations.length > 0) {
|
|
2153
|
+
pending = pending.filter(locationFilter);
|
|
2154
|
+
}
|
|
2155
|
+
// 未登录客户端不能领取 ttSeller 用户
|
|
2156
|
+
if (!loggedIn) {
|
|
2157
|
+
pending = pending.filter((u) => u.ttSeller !== true);
|
|
2158
|
+
}
|
|
2159
|
+
// status_code 过滤:只领取空值、0 或 209002 的任务
|
|
2160
|
+
pending = pending.filter(
|
|
2161
|
+
(u) =>
|
|
2162
|
+
u.statusCode == null ||
|
|
2163
|
+
u.statusCode === 0 ||
|
|
2164
|
+
(loggedIn && u.statusCode === 209002),
|
|
2165
|
+
);
|
|
2166
|
+
let hasVideo = pending.filter((u) => u.videoCount > 0);
|
|
2167
|
+
const next = pickCandidate(hasVideo) || pickCandidate(pending);
|
|
2168
|
+
|
|
2169
|
+
if (next) {
|
|
2170
|
+
next.status = "processing";
|
|
2171
|
+
markStatsDirty();
|
|
2172
|
+
next.claimedAt = now;
|
|
2173
|
+
next.claimedBy = userId;
|
|
2174
|
+
save();
|
|
2175
|
+
return {
|
|
2176
|
+
uniqueId: next.uniqueId,
|
|
2177
|
+
nickname: next.nickname,
|
|
2178
|
+
claimedAt: next.claimedAt,
|
|
2179
|
+
claimedBy: userId,
|
|
2180
|
+
};
|
|
2181
|
+
}
|
|
2182
|
+
return null;
|
|
2058
2183
|
}
|
|
2059
2184
|
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
let next = candidates.find((u) => u.pinned);
|
|
2185
|
+
return null;
|
|
2186
|
+
}
|
|
2063
2187
|
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2188
|
+
function debugClaimNextJob(
|
|
2189
|
+
userId,
|
|
2190
|
+
expireMs = 5 * 60 * 1000,
|
|
2191
|
+
locations = null,
|
|
2192
|
+
loggedIn = true,
|
|
2193
|
+
) {
|
|
2194
|
+
if (db) {
|
|
2195
|
+
const now = Date.now();
|
|
2196
|
+
const info = {
|
|
2197
|
+
path: "db",
|
|
2198
|
+
userId,
|
|
2199
|
+
expireMs,
|
|
2200
|
+
loggedIn,
|
|
2201
|
+
};
|
|
2202
|
+
|
|
2203
|
+
const ongoingRow = db
|
|
2204
|
+
.prepare(
|
|
2205
|
+
`
|
|
2206
|
+
SELECT *
|
|
2207
|
+
FROM jobs
|
|
2208
|
+
WHERE status = 'processing'
|
|
2209
|
+
AND claimed_by = ?
|
|
2210
|
+
AND claimed_at IS NOT NULL
|
|
2211
|
+
AND ? - claimed_at < ?
|
|
2212
|
+
ORDER BY claimed_at DESC
|
|
2213
|
+
LIMIT 1
|
|
2214
|
+
`,
|
|
2215
|
+
)
|
|
2216
|
+
.get(userId, now, expireMs);
|
|
2217
|
+
info.ongoing = ongoingRow
|
|
2218
|
+
? {
|
|
2219
|
+
uniqueId: ongoingRow.unique_id,
|
|
2220
|
+
claimedBy: ongoingRow.claimed_by,
|
|
2221
|
+
claimedAt: ongoingRow.claimed_at,
|
|
2222
|
+
}
|
|
2223
|
+
: null;
|
|
2224
|
+
|
|
2225
|
+
const tier1 = new Set(["PL", "NL", "BE"]);
|
|
2226
|
+
const tier2 = new Set(["DE", "FR", "IT", "IE", "ES"]);
|
|
2227
|
+
const normalizedLocations = Array.isArray(locations)
|
|
2228
|
+
? locations
|
|
2229
|
+
.map((loc) => String(loc).trim().toUpperCase())
|
|
2230
|
+
.filter(Boolean)
|
|
2231
|
+
: [];
|
|
2232
|
+
|
|
2233
|
+
function getLocationGroups() {
|
|
2234
|
+
const selected = normalizedLocations.length
|
|
2235
|
+
? normalizedLocations
|
|
2236
|
+
: null;
|
|
2237
|
+
const tier1List = selected
|
|
2238
|
+
? selected.filter((loc) => tier1.has(loc))
|
|
2239
|
+
: [...tier1];
|
|
2240
|
+
const tier2List = selected
|
|
2241
|
+
? selected.filter((loc) => tier2.has(loc))
|
|
2242
|
+
: [...tier2];
|
|
2243
|
+
const otherList = selected
|
|
2244
|
+
? selected.filter((loc) => !tier1.has(loc) && !tier2.has(loc))
|
|
2245
|
+
: null;
|
|
2246
|
+
const groups = [];
|
|
2247
|
+
if (tier1List.length > 0)
|
|
2248
|
+
groups.push({ type: "include", values: tier1List });
|
|
2249
|
+
if (tier2List.length > 0)
|
|
2250
|
+
groups.push({ type: "include", values: tier2List });
|
|
2251
|
+
if (selected) {
|
|
2252
|
+
if (otherList.length > 0)
|
|
2253
|
+
groups.push({ type: "include", values: otherList });
|
|
2254
|
+
} else {
|
|
2255
|
+
groups.push({ type: "exclude", values: [...tier1, ...tier2] });
|
|
2076
2256
|
}
|
|
2257
|
+
return groups;
|
|
2077
2258
|
}
|
|
2078
2259
|
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2260
|
+
const locationGroups = getLocationGroups();
|
|
2261
|
+
info.locationGroups = locationGroups;
|
|
2262
|
+
|
|
2263
|
+
function applyLocationGroup(where, args, group) {
|
|
2264
|
+
if (!group) return;
|
|
2265
|
+
if (group.type === "include") {
|
|
2266
|
+
where.push(
|
|
2267
|
+
`UPPER(COALESCE(guessed_location, '')) IN (${group.values.map(() => "?").join(", ")})`,
|
|
2268
|
+
);
|
|
2269
|
+
args.push(...group.values);
|
|
2270
|
+
return;
|
|
2271
|
+
}
|
|
2272
|
+
where.push(
|
|
2273
|
+
`UPPER(COALESCE(guessed_location, '')) NOT IN (${group.values.map(() => "?").join(", ")})`,
|
|
2082
2274
|
);
|
|
2083
|
-
|
|
2084
|
-
next = seed[0] || null;
|
|
2275
|
+
args.push(...group.values);
|
|
2085
2276
|
}
|
|
2086
2277
|
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
const
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2278
|
+
function queryPendingOne({ requireVideo, group, filters = [] }) {
|
|
2279
|
+
const where = ["status = 'pending'"];
|
|
2280
|
+
const args = [];
|
|
2281
|
+
if (!loggedIn) {
|
|
2282
|
+
where.push("COALESCE(tt_seller, 0) != 1");
|
|
2283
|
+
where.push("(status_code IS NULL OR status_code = 0)");
|
|
2284
|
+
} else {
|
|
2285
|
+
where.push(
|
|
2286
|
+
"(status_code IS NULL OR status_code = 0 OR status_code = 209002)",
|
|
2287
|
+
);
|
|
2288
|
+
}
|
|
2289
|
+
if (requireVideo) {
|
|
2290
|
+
where.push("COALESCE(video_count, 0) > 0");
|
|
2291
|
+
}
|
|
2292
|
+
applyLocationGroup(where, args, group);
|
|
2293
|
+
for (const filter of filters) {
|
|
2294
|
+
where.push(filter);
|
|
2295
|
+
}
|
|
2296
|
+
const sql = `
|
|
2297
|
+
SELECT *
|
|
2298
|
+
FROM jobs
|
|
2299
|
+
WHERE ${where.join(" AND ")}
|
|
2300
|
+
ORDER BY follower_count DESC, created_at ASC, unique_id ASC
|
|
2301
|
+
LIMIT 1
|
|
2302
|
+
`;
|
|
2303
|
+
const row = db.prepare(sql).get(...args);
|
|
2304
|
+
return { row, sql, args };
|
|
2094
2305
|
}
|
|
2095
2306
|
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
(
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2307
|
+
function queryPendingByGroup({ requireVideo, group, filters = [] }) {
|
|
2308
|
+
if (group?.type === "include" && group.values.length > 1) {
|
|
2309
|
+
for (const location of group.values) {
|
|
2310
|
+
const ret = queryPendingOne({
|
|
2311
|
+
requireVideo,
|
|
2312
|
+
group: { type: "include", values: [location] },
|
|
2313
|
+
filters,
|
|
2314
|
+
});
|
|
2315
|
+
if (ret.row) return ret;
|
|
2316
|
+
}
|
|
2317
|
+
return { row: null, sql: null, args: [] };
|
|
2318
|
+
}
|
|
2319
|
+
return queryPendingOne({ requireVideo, group, filters });
|
|
2104
2320
|
}
|
|
2105
2321
|
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2322
|
+
function findPinnedPending(requireVideo) {
|
|
2323
|
+
const where = ["status = 'pending'", "COALESCE(pinned, 0) = 1"];
|
|
2324
|
+
const args = [];
|
|
2325
|
+
if (!loggedIn) {
|
|
2326
|
+
where.push("COALESCE(tt_seller, 0) != 1");
|
|
2327
|
+
}
|
|
2328
|
+
if (requireVideo) {
|
|
2329
|
+
where.push("COALESCE(video_count, 0) > 0");
|
|
2330
|
+
}
|
|
2331
|
+
if (normalizedLocations.length > 0) {
|
|
2332
|
+
where.push(
|
|
2333
|
+
`UPPER(COALESCE(guessed_location, '')) IN (${normalizedLocations.map(() => "?").join(", ")})`,
|
|
2334
|
+
);
|
|
2335
|
+
args.push(...normalizedLocations);
|
|
2336
|
+
}
|
|
2337
|
+
const sql = `
|
|
2338
|
+
SELECT *
|
|
2339
|
+
FROM jobs
|
|
2340
|
+
WHERE ${where.join(" AND ")}
|
|
2341
|
+
ORDER BY created_at ASC, unique_id ASC
|
|
2342
|
+
LIMIT 1
|
|
2343
|
+
`;
|
|
2344
|
+
const row = db.prepare(sql).get(...args);
|
|
2345
|
+
return { row, sql, args };
|
|
2109
2346
|
}
|
|
2110
2347
|
|
|
2111
|
-
|
|
2112
|
-
|
|
2348
|
+
const expiredSql = `
|
|
2349
|
+
SELECT *
|
|
2350
|
+
FROM jobs
|
|
2351
|
+
WHERE status = 'processing'
|
|
2352
|
+
AND claimed_at IS NOT NULL
|
|
2353
|
+
AND ? - claimed_at > ?
|
|
2354
|
+
ORDER BY claimed_at ASC
|
|
2355
|
+
LIMIT 1
|
|
2356
|
+
`;
|
|
2357
|
+
const expiredRow = db.prepare(expiredSql).get(now, expireMs);
|
|
2358
|
+
info.expired = expiredRow
|
|
2359
|
+
? {
|
|
2360
|
+
uniqueId: expiredRow.unique_id,
|
|
2361
|
+
claimedBy: expiredRow.claimed_by,
|
|
2362
|
+
claimedAt: expiredRow.claimed_at,
|
|
2363
|
+
diffMs: now - expiredRow.claimed_at,
|
|
2364
|
+
}
|
|
2365
|
+
: null;
|
|
2113
2366
|
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
const next = pickCandidate(hasVideo) || pickCandidate(pending);
|
|
2367
|
+
info.requireVideoPasses = [];
|
|
2368
|
+
for (const requireVideo of [true, false]) {
|
|
2369
|
+
const pass = { requireVideo };
|
|
2370
|
+
const pinned = findPinnedPending(requireVideo);
|
|
2371
|
+
pass.pinned = pinned.row
|
|
2372
|
+
? {
|
|
2373
|
+
uniqueId: pinned.row.unique_id,
|
|
2374
|
+
sql: pinned.sql,
|
|
2375
|
+
args: pinned.args,
|
|
2376
|
+
}
|
|
2377
|
+
: null;
|
|
2126
2378
|
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2379
|
+
if (!pass.pinned) {
|
|
2380
|
+
for (const group of locationGroups) {
|
|
2381
|
+
const seed = queryPendingByGroup({
|
|
2382
|
+
requireVideo,
|
|
2383
|
+
group,
|
|
2384
|
+
filters: [
|
|
2385
|
+
"COALESCE(pinned, 0) = 0",
|
|
2386
|
+
`instr(COALESCE(sources, ''), '"seed"') > 0`,
|
|
2387
|
+
],
|
|
2388
|
+
});
|
|
2389
|
+
if (seed.row) {
|
|
2390
|
+
pass.seed = {
|
|
2391
|
+
uniqueId: seed.row.unique_id,
|
|
2392
|
+
group,
|
|
2393
|
+
sql: seed.sql,
|
|
2394
|
+
args: seed.args,
|
|
2395
|
+
};
|
|
2396
|
+
break;
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
if (!pass.pinned && !pass.seed && loggedIn) {
|
|
2402
|
+
for (const group of locationGroups) {
|
|
2403
|
+
const seller = queryPendingByGroup({
|
|
2404
|
+
requireVideo,
|
|
2405
|
+
group,
|
|
2406
|
+
filters: [
|
|
2407
|
+
"COALESCE(pinned, 0) = 0",
|
|
2408
|
+
"tt_seller = 1",
|
|
2409
|
+
"verified = 0",
|
|
2410
|
+
],
|
|
2411
|
+
});
|
|
2412
|
+
if (seller.row) {
|
|
2413
|
+
pass.seller = {
|
|
2414
|
+
uniqueId: seller.row.unique_id,
|
|
2415
|
+
group,
|
|
2416
|
+
sql: seller.sql,
|
|
2417
|
+
args: seller.args,
|
|
2418
|
+
};
|
|
2419
|
+
break;
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
if (!pass.pinned && !pass.seed && !pass.seller) {
|
|
2425
|
+
for (const group of locationGroups) {
|
|
2426
|
+
const follow = queryPendingByGroup({
|
|
2427
|
+
requireVideo,
|
|
2428
|
+
group,
|
|
2429
|
+
filters: [
|
|
2430
|
+
"COALESCE(pinned, 0) = 0",
|
|
2431
|
+
`(
|
|
2432
|
+
instr(COALESCE(sources, ''), '"following"') > 0
|
|
2433
|
+
OR instr(COALESCE(sources, ''), '"follower"') > 0
|
|
2434
|
+
)`,
|
|
2435
|
+
],
|
|
2436
|
+
});
|
|
2437
|
+
if (follow.row) {
|
|
2438
|
+
pass.follow = {
|
|
2439
|
+
uniqueId: follow.row.unique_id,
|
|
2440
|
+
group,
|
|
2441
|
+
sql: follow.sql,
|
|
2442
|
+
args: follow.args,
|
|
2443
|
+
};
|
|
2444
|
+
break;
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
if (!pass.pinned && !pass.seed && !pass.seller && !pass.follow) {
|
|
2450
|
+
for (const group of locationGroups) {
|
|
2451
|
+
const other = queryPendingByGroup({
|
|
2452
|
+
requireVideo,
|
|
2453
|
+
group,
|
|
2454
|
+
filters: ["COALESCE(pinned, 0) = 0"],
|
|
2455
|
+
});
|
|
2456
|
+
if (other.row) {
|
|
2457
|
+
pass.other = {
|
|
2458
|
+
uniqueId: other.row.unique_id,
|
|
2459
|
+
group,
|
|
2460
|
+
sql: other.sql,
|
|
2461
|
+
args: other.args,
|
|
2462
|
+
};
|
|
2463
|
+
break;
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
info.requireVideoPasses.push(pass);
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
return info;
|
|
2139
2472
|
}
|
|
2140
|
-
|
|
2473
|
+
|
|
2474
|
+
return {
|
|
2475
|
+
path: "memory",
|
|
2476
|
+
userId,
|
|
2477
|
+
expireMs,
|
|
2478
|
+
loggedIn,
|
|
2479
|
+
totalUsers: data.length,
|
|
2480
|
+
processingUsers: data.filter((u) => u.status === "processing").length,
|
|
2481
|
+
pendingUsers: data.filter((u) => u.status === "pending").length,
|
|
2482
|
+
};
|
|
2141
2483
|
}
|
|
2142
2484
|
|
|
2143
2485
|
function processDiscoveredUsers(result) {
|
|
@@ -2246,6 +2588,7 @@ export function createStore(filePath) {
|
|
|
2246
2588
|
}
|
|
2247
2589
|
}
|
|
2248
2590
|
}
|
|
2591
|
+
user.restricted = true;
|
|
2249
2592
|
user.processed = true;
|
|
2250
2593
|
user.processedAt = Date.now();
|
|
2251
2594
|
user.sources = [...new Set([...(user.sources || []), "restricted"])];
|
|
@@ -2795,9 +3138,10 @@ export function createStore(filePath) {
|
|
|
2795
3138
|
location_created,
|
|
2796
3139
|
tt_seller,
|
|
2797
3140
|
registered_at,
|
|
2798
|
-
user_update_count
|
|
3141
|
+
user_update_count,
|
|
3142
|
+
create_time
|
|
2799
3143
|
)
|
|
2800
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
3144
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
2801
3145
|
`);
|
|
2802
3146
|
let registered = 0;
|
|
2803
3147
|
let skipped = 0;
|
|
@@ -2812,6 +3156,7 @@ export function createStore(filePath) {
|
|
|
2812
3156
|
ttSeller ? 1 : 0,
|
|
2813
3157
|
now,
|
|
2814
3158
|
0,
|
|
3159
|
+
item.createTime || null,
|
|
2815
3160
|
);
|
|
2816
3161
|
if (result.changes > 0) registered++;
|
|
2817
3162
|
else skipped++;
|
|
@@ -2837,6 +3182,7 @@ export function createStore(filePath) {
|
|
|
2837
3182
|
locationCreated: locationCreated || null,
|
|
2838
3183
|
ttSeller: ttSeller || false,
|
|
2839
3184
|
registeredAt: Date.now(),
|
|
3185
|
+
createTime: item.createTime || null,
|
|
2840
3186
|
});
|
|
2841
3187
|
existingIds.add(item.id);
|
|
2842
3188
|
registered++;
|
|
@@ -3024,7 +3370,9 @@ export function createStore(filePath) {
|
|
|
3024
3370
|
getVideoCount,
|
|
3025
3371
|
getPendingCommentTasks,
|
|
3026
3372
|
commitCommentTask,
|
|
3373
|
+
debugClaimNextJob,
|
|
3027
3374
|
stopBackup,
|
|
3375
|
+
rawQuery,
|
|
3028
3376
|
data,
|
|
3029
3377
|
};
|
|
3030
3378
|
}
|