Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add api for subject collect #944

Merged
merged 78 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
58dc28f
z
everpcpc Jan 26, 2025
8dbc75a
z
everpcpc Jan 26, 2025
7a7eb79
Merge remote-tracking branch 'upstream/master' into feat-update
everpcpc Jan 31, 2025
c0f512b
z
everpcpc Jan 31, 2025
819a198
Merge remote-tracking branch 'upstream/master' into feat-update
everpcpc Jan 31, 2025
82b82a3
z
everpcpc Jan 31, 2025
24ff698
Merge remote-tracking branch 'upstream/master' into feat-update
everpcpc Feb 2, 2025
4ce9000
z
everpcpc Feb 2, 2025
31dc44c
z
everpcpc Feb 2, 2025
c728c57
z
everpcpc Feb 2, 2025
618d8e0
z
everpcpc Feb 2, 2025
7d892ed
z
everpcpc Feb 2, 2025
d2b59b3
z
everpcpc Feb 2, 2025
1ce7797
z
everpcpc Feb 2, 2025
fb58128
z
everpcpc Feb 4, 2025
eb5acfd
Merge remote-tracking branch 'upstream/master' into feat-update
everpcpc Feb 4, 2025
186e7ee
Merge remote-tracking branch 'upstream/master' into feat-update
everpcpc Feb 4, 2025
a670bc1
z
everpcpc Feb 4, 2025
c230598
Merge remote-tracking branch 'upstream/master' into feat-update
everpcpc Feb 7, 2025
131a0a3
Merge remote-tracking branch 'upstream/master' into feat-update
everpcpc Feb 7, 2025
e911a8d
Merge remote-tracking branch 'upstream/master' into feat-update
everpcpc Feb 7, 2025
cc36e5d
z
everpcpc Feb 7, 2025
e85b77d
z
everpcpc Feb 7, 2025
4416ed0
z
everpcpc Feb 7, 2025
e333175
z
everpcpc Feb 7, 2025
74aa259
z
everpcpc Feb 7, 2025
b14f740
z
everpcpc Feb 7, 2025
6ba2d27
z
everpcpc Feb 8, 2025
59167b3
z
everpcpc Feb 8, 2025
63400d8
z
everpcpc Feb 8, 2025
13d0cdb
Merge remote-tracking branch 'upstream/master' into feat-update
everpcpc Feb 8, 2025
12a6c1f
z
everpcpc Feb 8, 2025
3fedfc1
z
everpcpc Feb 8, 2025
7c29aad
z
everpcpc Feb 8, 2025
fc26af1
z
everpcpc Feb 8, 2025
583a0bd
z
everpcpc Feb 8, 2025
b3c981a
z
everpcpc Feb 8, 2025
b1d4409
Merge remote-tracking branch 'upstream/master' into feat-update
everpcpc Feb 8, 2025
dac6579
z
everpcpc Feb 9, 2025
7df87a7
z
everpcpc Feb 9, 2025
3a36580
z
everpcpc Feb 9, 2025
9be7aa8
Merge remote-tracking branch 'upstream/master' into feat-update
everpcpc Feb 9, 2025
6299238
z
everpcpc Feb 9, 2025
f913d9f
z
everpcpc Feb 9, 2025
b127854
z
everpcpc Feb 9, 2025
a9c9a37
z
everpcpc Feb 9, 2025
51c7634
z
everpcpc Feb 9, 2025
7d92190
Merge remote-tracking branch 'upstream/master' into feat-update
everpcpc Feb 9, 2025
d298e09
z
everpcpc Feb 9, 2025
f2ae030
Merge remote-tracking branch 'upstream/master' into feat-update
everpcpc Feb 9, 2025
fed0bd2
z
everpcpc Feb 9, 2025
960b117
z
everpcpc Feb 9, 2025
6d9ebed
z
everpcpc Feb 9, 2025
5a7f215
z
everpcpc Feb 9, 2025
4348374
z
everpcpc Feb 10, 2025
d8d9f42
z
everpcpc Feb 10, 2025
4aecd82
z
everpcpc Feb 10, 2025
569fa5a
z
everpcpc Feb 10, 2025
fda002d
z
everpcpc Feb 10, 2025
6279ce8
z
everpcpc Feb 10, 2025
268ad83
z
everpcpc Feb 10, 2025
7480f95
z
everpcpc Feb 10, 2025
82d19d4
z
everpcpc Feb 10, 2025
2fc30dd
z
everpcpc Feb 10, 2025
b8a4440
z
everpcpc Feb 10, 2025
d413bd6
z
everpcpc Feb 10, 2025
0934f9f
z
everpcpc Feb 10, 2025
06a39a9
z
everpcpc Feb 10, 2025
9e7a930
z
everpcpc Feb 10, 2025
3db5db4
z
everpcpc Feb 10, 2025
e6dd994
z
everpcpc Feb 10, 2025
98a57b6
z
everpcpc Feb 10, 2025
6233572
z
everpcpc Feb 10, 2025
b824263
z
everpcpc Feb 10, 2025
38a2f3b
z
everpcpc Feb 10, 2025
58a9b7e
z
everpcpc Feb 10, 2025
29dbedb
z
everpcpc Feb 10, 2025
c281ef8
z
everpcpc Feb 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions drizzle/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@ export const chiiSubjectImgs = mysqlTable('chii_subject_imgs', {
});

export const chiiSubjectInterests = mysqlTable('chii_subject_interests', {
id: int('interest_id').autoincrement().notNull(),
id: int('interest_id').autoincrement().notNull().primaryKey(),
uid: mediumint('interest_uid').notNull(),
subjectID: mediumint('interest_subject_id').notNull(),
subjectType: smallint('interest_subject_type').notNull(),
Expand Down Expand Up @@ -650,11 +650,11 @@ export const chiiSubjectPosts = mysqlTable('chii_subject_posts', {
});

export const chiiTagIndex = mysqlTable('chii_tag_neue_index', {
id: mediumint('tag_id').autoincrement().notNull(),
id: mediumint('tag_id').autoincrement().notNull().primaryKey(),
name: varchar('tag_name', { length: 30 }).notNull(),
cat: tinyint('tag_cat').notNull(),
type: tinyint('tag_type').notNull(),
totalCount: mediumint('tag_results').notNull(),
count: mediumint('tag_results').notNull(),
createdAt: int('tag_dateline').notNull(),
updatedAt: int('tag_lasttouch').notNull(),
});
Expand Down
3 changes: 2 additions & 1 deletion lib/subject/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ import { RevType } from '@app/lib/orm/entity/index.ts';
import * as ormold from '@app/lib/orm/index.ts';
import { SubjectImageRepo, SubjectRepo } from '@app/lib/orm/index.ts';
import { extractDate } from '@app/lib/subject/date.ts';
import { TagCat } from '@app/lib/tag.ts';
import * as fetcher from '@app/lib/types/fetcher.ts';
import { DATE } from '@app/lib/utils/date.ts';
import { matchExpected } from '@app/lib/wiki.ts';
import { getSubjectPlatforms } from '@app/vendor';

import { SubjectType, TagCat } from './type.ts';
import { SubjectType } from './type.ts';

export const InvalidWikiSyntaxError = createError(
'INVALID_SYNTAX_ERROR',
Expand Down
9 changes: 2 additions & 7 deletions lib/subject/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ export enum EpisodeCollectionStatus {
Dropped = 3, // 抛弃
}

export interface UserEpisodeCollection {
id: number;
export interface UserEpisodeStatusItem {
eid: number;
type: EpisodeCollectionStatus;
}

Expand All @@ -74,11 +74,6 @@ export enum PersonType {
Person = 'prsn',
}

export enum TagCat {
Subject = 0,
Meta = 3,
}

export enum SubjectSort {
Rank = 'rank',
Trends = 'trends',
Expand Down
164 changes: 164 additions & 0 deletions lib/subject/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import * as php from '@trim21/php-serialize';
import { DateTime } from 'luxon';

import { db, decr, incr, op, type Txn } from '@app/drizzle/db.ts';
import type * as orm from '@app/drizzle/orm.ts';
import * as schema from '@app/drizzle/schema';

import type { CollectionType, UserEpisodeStatusItem } from './type';
import { EpisodeCollectionStatus, getCollectionTypeField } from './type';

/** 更新条目收藏计数,需要在事务中执行 */
export async function updateSubjectCollection(
t: Txn,
subjectID: number,
newType: CollectionType,
oldType?: CollectionType,
) {
if (oldType && oldType === newType) {
return;
}
const toUpdate: Record<string, op.SQL> = {};
toUpdate[getCollectionTypeField(newType)] = incr(
schema.chiiSubjects[getCollectionTypeField(newType)],
);
if (oldType) {
toUpdate[getCollectionTypeField(oldType)] = decr(
schema.chiiSubjects[getCollectionTypeField(oldType)],
);
}
await t
.update(schema.chiiSubjects)
.set(toUpdate)
.where(op.eq(schema.chiiSubjects.id, subjectID))
.limit(1);
}

function getRatingField(rate: number) {
return [
undefined,
'rate1',
'rate2',
'rate3',
'rate4',
'rate5',
'rate6',
'rate7',
'rate8',
'rate9',
'rate10',
][rate];
}

/** 更新条目评分,需要在事务中执行 */
export async function updateSubjectRating(
t: Txn,
subjectID: number,
oldRate: number,
newRate: number,
) {
if (oldRate === newRate) {
return;
}
const newField = getRatingField(newRate);
const oldField = getRatingField(oldRate);
const toUpdate: Record<string, op.SQL> = {};
if (newField) {
const field = newField as keyof orm.ISubjectFields;
toUpdate[field] = incr(schema.chiiSubjectFields[field]);
}
if (oldField) {
const field = oldField as keyof orm.ISubjectFields;
toUpdate[field] = decr(schema.chiiSubjectFields[field]);
}
if (Object.keys(toUpdate).length === 0) {
return;
}
await t
.update(schema.chiiSubjects)
.set(toUpdate)
.where(op.eq(schema.chiiSubjects.id, subjectID))
.limit(1);
}

/** 标记条目剧集为已观看,需要在事务中执行 */
export async function markEpisodesAsWatched(
t: Txn,
userID: number,
subjectID: number,
episodeIDs: number[],
revertOthers = false,
) {
const epStatusList: Record<number, UserEpisodeStatusItem> = {};
for (const episodeID of episodeIDs) {
epStatusList[episodeID] = {
eid: episodeID,
type: EpisodeCollectionStatus.Done,
};
}
const [current] = await t
.select()
.from(schema.chiiEpStatus)
.where(
op.and(op.eq(schema.chiiEpStatus.uid, userID), op.eq(schema.chiiEpStatus.sid, subjectID)),
);
if (current?.status) {
const oldList = parseSubjectEpStatus(current.status);
for (const x of Object.values(oldList)) {
if (episodeIDs.includes(x.eid)) {
continue;
}
if (revertOthers && x.type === EpisodeCollectionStatus.Done) {
epStatusList[x.eid] = {
eid: x.eid,
type: EpisodeCollectionStatus.None,
};
} else {
epStatusList[x.eid] = x;
}
}
const newStatus = php.stringify(epStatusList);
await t
.update(schema.chiiEpStatus)
.set({ status: newStatus, updatedAt: DateTime.now().toUnixInteger() })
.where(op.eq(schema.chiiEpStatus.id, current.id))
.limit(1);
} else {
const newStatus = php.stringify(epStatusList);
await t.insert(schema.chiiEpStatus).values({
uid: userID,
sid: subjectID,
status: newStatus,
updatedAt: DateTime.now().toUnixInteger(),
});
}
}

export function parseSubjectEpStatus(status: string): Record<number, UserEpisodeStatusItem> {
const result: Record<number, UserEpisodeStatusItem> = {};
if (!status) {
return result;
}
const epStatusList = php.parse(status) as Record<number, UserEpisodeStatusItem>;
for (const x of Object.values(epStatusList)) {
result[x.eid] = x;
}
return result;
}

export async function getEpStatus(
userID: number,
subjectID: number,
): Promise<Record<number, UserEpisodeStatusItem>> {
const [data] = await db
.select()
.from(schema.chiiEpStatus)
.where(
op.and(op.eq(schema.chiiEpStatus.uid, userID), op.eq(schema.chiiEpStatus.sid, subjectID)),
)
.limit(1);
if (!data) {
return {};
}
return parseSubjectEpStatus(data.status);
}
162 changes: 162 additions & 0 deletions lib/tag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { DateTime } from 'luxon';

import type { Txn } from '@app/drizzle/db.ts';
import { op } from '@app/drizzle/db.ts';
import * as schema from '@app/drizzle/schema.ts';
import { dam } from '@app/lib/dam';

export enum TagCat {
/** 条目, 对应的 type: 条目类型 */
Subject = 0,

/** 入口, 对应的 type: blog = 1 */
Entry = 1,

/** 同人, 对应的 type: doujin = 1 和 club = 2 */
Doujin = 2,

/** Wiki, 对应的 type: 条目类型 */
Meta = 3,
}

export function validateTags(tags: string[]): string[] {
const result = new Set<string>();
for (const tag of tags) {
const t = tag.trim().normalize('NFKC');
if (t.length < 2) {
continue;
}
if (dam.needReview(t)) {
continue;
}
result.add(t);
if (result.size >= 10) {
break;
}
}
return [...result].sort();
}

/**
* 插入用户收藏标签,需要在事务中执行
*
* @param t - 事务
* @param uid - 用户ID
* @param cat - 标签分类
* @param type - 标签类型
* @param mid - 条目 ID 等
* @param tags - 输入的标签
* @returns 清理后的标签
*/
export async function insertUserTags(
t: Txn,
uid: number,
cat: TagCat,
type: number,
mid: number,
tags: string[],
): Promise<string[]> {
tags = validateTags(tags);
await t
.delete(schema.chiiTagList)
.where(
op.and(
op.eq(schema.chiiTagList.userID, uid),
op.eq(schema.chiiTagList.cat, cat),
op.eq(schema.chiiTagList.type, type),
op.eq(schema.chiiTagList.mainID, mid),
),
);

const tagIDs = await ensureTags(t, cat, type, tags);
const tids = Object.values(tagIDs).sort();

if (tids.length > 0) {
const now = DateTime.now().toUnixInteger();
await t.insert(schema.chiiTagList).values(
tids.map((id) => ({
tagID: id,
userID: uid,
cat,
type,
mainID: mid,
createdAt: now,
})),
);
await updateTagResult(t, tids);
}
return tags;
}

export async function updateTagResult(t: Txn, tagIDs: number[]) {
const now = DateTime.now().toUnixInteger();
const counts = await t
.select({
tagID: schema.chiiTagList.tagID,
count: op.count(schema.chiiTagList.tagID),
})
.from(schema.chiiTagList)
.where(op.inArray(schema.chiiTagList.tagID, tagIDs))
.groupBy(schema.chiiTagList.tagID);
for (const item of counts) {
await t
.update(schema.chiiTagIndex)
.set({
count: item.count,
updatedAt: now,
})
.where(op.eq(schema.chiiTagIndex.id, item.tagID))
.limit(1);
}
}

export async function ensureTags(
t: Txn,
cat: TagCat,
type: number,
tags: string[],
): Promise<Record<string, number>> {
const tagIDs: Record<string, number> = {};
if (tags.length === 0) {
return tagIDs;
}

const existTags = await t
.select()
.from(schema.chiiTagIndex)
.where(
op.and(
op.eq(schema.chiiTagIndex.cat, cat),
op.eq(schema.chiiTagIndex.type, type),
op.inArray(schema.chiiTagIndex.name, tags),
),
);
for (const tag of existTags) {
tagIDs[tag.name] = tag.id;
}

const now = DateTime.now().toUnixInteger();
const insertTags = tags.filter((tag) => !tagIDs[tag]);
if (insertTags.length > 0) {
const insertResult = await t
.insert(schema.chiiTagIndex)
.values(
insertTags.map((tag) => ({
name: tag,
cat: cat,
type: type,
count: 0,
createdAt: now,
updatedAt: now,
})),
)
.$returningId();
for (const [idx, r] of insertResult.entries()) {
const tag = insertTags[idx];
if (tag) {
tagIDs[tag] = r.id;
}
}
}
return tagIDs;
}
Loading
Loading