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.
@@ -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
- // 未登录用户只能领取 statusCode 为空的任务(209002 只能被登录用户领取)
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
- // 登录用户可以领取 statusCode 为空 或 statusCode=209002 的任务
1845
- where.push("status_code IS NULL OR status_code = 209002");
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
- const now = Date.now();
2056
+ if (!db) {
2057
+ const now = Date.now();
2024
2058
 
2025
- // 0. 该客户端有未过期的任务,续期返回
2026
- const ongoing = data.find(
2027
- (u) =>
2028
- u.status === "processing" &&
2029
- u.claimedBy === userId &&
2030
- u.claimedAt &&
2031
- now - u.claimedAt < expireMs,
2032
- );
2033
- if (ongoing) {
2034
- ongoing.claimedAt = now;
2035
- save();
2036
- return {
2037
- uniqueId: ongoing.uniqueId,
2038
- nickname: ongoing.nickname,
2039
- claimedAt: ongoing.claimedAt,
2040
- claimedBy: userId,
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
- const tier1 = new Set(["PL", "NL", "BE"]);
2046
- const tier2 = new Set(["DE", "FR", "IT", "IE", "ES"]);
2047
- function locationTier(u) {
2048
- const loc = (u.guessedLocation || "").toUpperCase();
2049
- if (tier1.has(loc)) return 0;
2050
- if (tier2.has(loc)) return 1;
2051
- return 2;
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
- // 国家过滤:如果指定了 locations,只保留 guessedLocation 在列表中的用户
2055
- function locationFilter(u) {
2056
- if (!locations || locations.length === 0) return true;
2057
- return isLocationInList(u.guessedLocation, locations);
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
- // 从候选列表中按优先级取第一个:pinned > 超时回收 > seed > ttSeller(仅登录) > follow > other
2061
- function pickCandidate(candidates) {
2062
- let next = candidates.find((u) => u.pinned);
2185
+ return null;
2186
+ }
2063
2187
 
2064
- if (!next) {
2065
- const expired = data.find(
2066
- (u) =>
2067
- u.status === "processing" &&
2068
- u.claimedAt &&
2069
- now - u.claimedAt > expireMs,
2070
- );
2071
- if (expired) {
2072
- expired.status = "pending";
2073
- markStatsDirty();
2074
- delete expired.claimedAt;
2075
- next = expired;
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
- if (!next) {
2080
- const seed = candidates.filter(
2081
- (u) => u.sources && u.sources.includes("seed"),
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
- seed.sort((a, b) => locationTier(a) - locationTier(b));
2084
- next = seed[0] || null;
2275
+ args.push(...group.values);
2085
2276
  }
2086
2277
 
2087
- // 未登录时跳过 ttSeller 优先级
2088
- if (!next && loggedIn) {
2089
- const ttSeller = candidates.filter(
2090
- (u) => u.ttSeller === true && u.verified === false,
2091
- );
2092
- ttSeller.sort((a, b) => locationTier(a) - locationTier(b));
2093
- next = ttSeller[0] || null;
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
- if (!next) {
2097
- const follow = candidates.filter(
2098
- (u) =>
2099
- u.sources &&
2100
- (u.sources.includes("following") || u.sources.includes("follower")),
2101
- );
2102
- follow.sort((a, b) => locationTier(a) - locationTier(b));
2103
- next = follow[0] || null;
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
- if (!next) {
2107
- candidates.sort((a, b) => locationTier(a) - locationTier(b));
2108
- next = candidates[0] || null;
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
- return next;
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
- // 先在有视频的 pending 用户中找;找不到再用全部 pending 用户兜底
2115
- let pending = data.filter((u) => u.status === "pending");
2116
- // 应用国家过滤
2117
- if (locations && locations.length > 0) {
2118
- pending = pending.filter(locationFilter);
2119
- }
2120
- // 未登录客户端不能领取 ttSeller 用户
2121
- if (!loggedIn) {
2122
- pending = pending.filter((u) => u.ttSeller !== true);
2123
- }
2124
- let hasVideo = pending.filter((u) => u.videoCount > 0);
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
- if (next) {
2128
- next.status = "processing";
2129
- markStatsDirty();
2130
- next.claimedAt = now;
2131
- next.claimedBy = userId;
2132
- save();
2133
- return {
2134
- uniqueId: next.uniqueId,
2135
- nickname: next.nickname,
2136
- claimedAt: next.claimedAt,
2137
- claimedBy: userId,
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
- return null;
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
  }