Add audiobooks

This commit is contained in:
2024-06-15 11:53:03 +04:00
parent 1374c2bd70
commit f43dc5f11b
43 changed files with 1651 additions and 625 deletions

View File

@@ -1,5 +1,7 @@
import { HTTPService } from "@/shared/utils/http";
import { coverNameSchema } from "./schemas/cover";
import { torrentNameSchema } from "./schemas/torrent";
import { fragmentNameSchema } from "./schemas/fragment";
export abstract class FilesService {
public static async UploadCover(cover: File) {
@@ -19,7 +21,19 @@ export abstract class FilesService {
formData.append("torrent", torrent);
return await HTTPService.post(
`/files/torrent`,
coverNameSchema,
torrentNameSchema,
formData,
{},
false
);
}
public static async UploadFragment(fragment: File) {
const formData = new FormData();
formData.append("fragment", fragment);
return await HTTPService.post(
`/files/audio`,
fragmentNameSchema,
formData,
{},
false

View File

@@ -0,0 +1,4 @@
import { z } from "zod";
export const fragmentNameSchema = z.string().min(5);
export type FragmentNameType = z.infer<typeof fragmentNameSchema>;

View File

@@ -0,0 +1,4 @@
import { z } from "zod";
export const torrentNameSchema = z.string().min(5);
export type TorrentNameType = z.infer<typeof torrentNameSchema>;

View File

@@ -1,25 +0,0 @@
import { HTTPService } from "@/shared/utils/http";
import { gameCardsSchema } from "./schemas/gameCard";
import { GameCreateType, gameSchema } from "./schemas/game";
export abstract class GameService {
public static async GetGameCards() {
return await HTTPService.get("/games/cards", gameCardsSchema);
}
public static async GetGame(id: number) {
return await HTTPService.get(`/games/${id}`, gameSchema);
}
public static async ChangeGame(id: number, gameInfo: GameCreateType) {
return await HTTPService.put(`/games/${id}`, gameSchema, gameInfo);
}
public static async AddGame(gameInfo: GameCreateType) {
return await HTTPService.post(`/games`, gameSchema, gameInfo);
}
public static GetEmptyGame(): GameCreateType {
return {
title: "",
torrent_file: "",
};
}
}

View File

@@ -1,11 +0,0 @@
import { GameCardType } from "./schemas/gameCard";
import { GameType, GameCreateType, gameCreateSchema } from "./schemas/game";
import { GameService } from "./game";
export {
GameService,
gameCreateSchema,
type GameType,
type GameCreateType,
type GameCardType,
};

View File

@@ -0,0 +1,66 @@
import { HTTPService } from "@/shared/utils/http";
import { audiobookCardsSchema } from "./schemas/audiobookCard";
import {
AudiobookCreateType,
audiobookSchema,
AudiobookType,
} from "./schemas/audiobook";
import {
IItemService,
ItemCreateType,
ItemPropertiesDescriptionType,
ItemType,
staticImplements,
TypesOfItems,
} from "../types";
import { ItemService } from "../item";
@staticImplements<IItemService>()
export abstract class AudiobookService {
public static async GetCards() {
return await HTTPService.get("/audiobooks/cards", audiobookCardsSchema);
}
public static async Get(id: number) {
return await HTTPService.get(`/audiobooks/${id}`, audiobookSchema);
}
public static async Add(info: AudiobookCreateType) {
return await HTTPService.post(`/audiobooks`, audiobookSchema, info);
}
public static async Change(id: number, info: AudiobookCreateType) {
return await HTTPService.put(`/audiobooks/${id}`, audiobookSchema, info);
}
public static GetEmpty(): AudiobookCreateType {
return {
title: "",
torrent_file: "",
type: TypesOfItems.audiobook,
};
}
static propertiesDescription: ItemPropertiesDescriptionType<AudiobookType> = [
[
{ name: "Автор", key: "author" },
{ name: "Язык", key: "language" },
{ name: "Читает", key: "reader" },
{ name: "Продолжительность", key: "duration" },
],
[
{
name: "Дата обновления раздачи",
key: "update_date",
value: (item: ItemType | ItemCreateType) => {
return ItemService.isExistingItem(item)
? item.update_date.toLocaleDateString("ru-ru")
: new Date().toLocaleDateString("ru-ru");
},
editable: false,
},
{
name: "Год выхода",
key: "release_date",
},
{ name: "Объём загрузки", key: "download_size" },
],
];
}

View File

@@ -0,0 +1,58 @@
import { z } from "zod";
import { audiobookCardBaseSchema } from "./audiobookCard";
export const audiobookBaseSchema = audiobookCardBaseSchema.merge(
z.object({
torrent_file: z.string().min(3, "У раздачи должен быть .torrent файл"),
fragment: z.string().optional().nullable(),
language: z.string().optional().nullable(),
download_size: z.string().optional().nullable(),
duration: z.string().optional().nullable(),
reader: z.string().optional().nullable(),
release_date: z
.string()
.optional()
.nullable()
.transform((d) =>
d
? new Date(d).toLocaleDateString("en-us", {
year: "numeric",
})
: undefined
),
})
);
export const audiobookCreateSchema = audiobookBaseSchema.merge(z.object({}));
export type AudiobookCreateType = z.infer<typeof audiobookCreateSchema>;
export const audiobookSchema = audiobookBaseSchema.merge(
z.object({
id: z.number().positive(),
owner_id: z.number().positive(),
update_date: z
.string()
.min(1)
.transform((d) => new Date(d)),
upload_date: z
.string()
.min(1)
.transform((d) => new Date(d)),
})
);
export type AudiobookType = z.infer<typeof audiobookSchema>;
export const isAudiobook = (a: any): a is AudiobookType => {
return audiobookSchema.safeParse(a).success;
};
export const audiobooksSchema = z.array(z.any()).transform((a) => {
const audiobooks: AudiobookType[] = [];
a.forEach((e) => {
if (isAudiobook(e)) audiobooks.push(audiobookSchema.parse(e));
else console.error("Audiobook parse error - ", e);
});
return audiobooks;
});

View File

@@ -0,0 +1,43 @@
import { z } from "zod";
import { TypesOfItems } from "../../types";
export const audiobookCardBaseSchema = z.object({
title: z.string().min(3, "Слишком короткое название"),
cover: z.string().optional().nullable(),
description: z.string().optional().nullable(),
author: z.string().optional().nullable(),
// Добавляем к каждой аудиокниге поле, которое
// показывает, что item является аудиокнигой
type: z
.any()
.optional()
.transform(() => TypesOfItems.audiobook),
});
export const audiobookCardSchema = audiobookCardBaseSchema.merge(
z.object({
id: z.number().positive(),
})
);
export type AudiobookCardType = z.infer<typeof audiobookCardSchema>;
export const isAudiobookCardStrict = (a: any): a is AudiobookCardType => {
return audiobookCardSchema.safeParse(a).success;
};
export const audiobookCardsSchema = z.array(z.any()).transform((a) => {
const cards: AudiobookCardType[] = [];
a.forEach((e) => {
if (isAudiobookCardStrict(e)) cards.push(audiobookCardSchema.parse(e));
else console.error("AudiobookCard parse error - ", e);
});
return cards;
});
export const isAudiobook = (a: any): a is AudiobookCardType => {
return (
audiobookCardBaseSchema.safeParse(a).success &&
(a as AudiobookCardType).type === TypesOfItems.audiobook
);
};

View File

@@ -0,0 +1,66 @@
import { HTTPService } from "@/shared/utils/http";
import { gameCardsSchema } from "./schemas/gameCard";
import { GameCreateType, gameSchema, GameType } from "./schemas/game";
import {
IItemService,
ItemCreateType,
ItemPropertiesDescriptionType,
ItemType,
staticImplements,
TypesOfItems,
} from "../types";
import { ItemService } from "../item";
@staticImplements<IItemService>()
export abstract class GameService {
public static async GetCards() {
return await HTTPService.get("/games/cards", gameCardsSchema);
}
public static async Get(id: number) {
return await HTTPService.get(`/games/${id}`, gameSchema);
}
public static async Add(info: GameCreateType) {
return await HTTPService.post(`/games`, gameSchema, info);
}
public static async Change(id: number, info: GameCreateType) {
return await HTTPService.put(`/games/${id}`, gameSchema, info);
}
public static GetEmpty(): GameCreateType {
return {
title: "",
torrent_file: "",
type: TypesOfItems.game,
};
}
static propertiesDescription: ItemPropertiesDescriptionType<GameType> = [
[
{ name: "Система", key: "system" },
{ name: "Процессор", key: "processor" },
{ name: "Оперативная память", key: "memory" },
{ name: "Видеокарта", key: "graphics" },
{ name: "Место на диске", key: "storage" },
],
[
{ name: "Версия игры", key: "version" },
{
name: "Дата обновления раздачи",
key: "update_date",
value: (item: ItemType | ItemCreateType) => {
return ItemService.isExistingItem(item)
? item.update_date.toLocaleDateString("ru-ru")
: new Date().toLocaleDateString("ru-ru");
},
editable: false,
},
{ name: "Язык", key: "language" },
{ name: "Разработчик", key: "developer" },
{
name: "Год выхода",
key: "release_date",
},
{ name: "Объём загрузки", key: "download_size" },
],
];
}

View File

@@ -1,10 +1,18 @@
import { z } from "zod";
import { TypesOfItems } from "../../types";
export const gameCardBaseSchema = z.object({
title: z.string().min(3, "Слишком короткое название"),
cover: z.string().optional().nullable(),
description: z.string().optional().nullable(),
version: z.string().optional().nullable(),
// Добавляем к каждой игре поле, которое
// показывает, что item является игрой
type: z
.any()
.optional()
.transform(() => TypesOfItems.game),
});
export const gameCardSchema = gameCardBaseSchema.merge(
@@ -14,15 +22,22 @@ export const gameCardSchema = gameCardBaseSchema.merge(
);
export type GameCardType = z.infer<typeof gameCardSchema>;
export const isGameCard = (a: any): a is GameCardType => {
export const isGameCardStrict = (a: any): a is GameCardType => {
return gameCardSchema.safeParse(a).success;
};
export const gameCardsSchema = z.array(z.any()).transform((a) => {
const cards: GameCardType[] = [];
a.forEach((e) => {
if (isGameCard(e)) cards.push(gameCardSchema.parse(e));
if (isGameCardStrict(e)) cards.push(gameCardSchema.parse(e));
else console.error("GameCard parse error - ", e);
});
return cards;
});
export const isGame = (a: any): a is GameCardType => {
return (
gameCardBaseSchema.safeParse(a).success &&
(a as GameCardType).type === TypesOfItems.game
);
};

104
src/entities/item/index.ts Normal file
View File

@@ -0,0 +1,104 @@
import {
gameSchema,
gamesSchema,
gameCreateSchema,
type GameType,
type GameCreateType,
} from "./game/schemas/game";
export {
gameSchema,
gamesSchema,
gameCreateSchema,
type GameType,
type GameCreateType,
};
import {
gameCardSchema,
gameCardsSchema,
type GameCardType,
isGame,
} from "./game/schemas/gameCard";
export { gameCardSchema, gameCardsSchema, type GameCardType, isGame };
import { GameService } from "./game/game";
export { GameService };
import {
movieSchema,
moviesSchema,
movieCreateSchema,
type MovieType,
type MovieCreateType,
} from "./movie/schemas/movie";
export {
movieSchema,
moviesSchema,
movieCreateSchema,
type MovieType,
type MovieCreateType,
};
import {
movieCardSchema,
movieCardsSchema,
type MovieCardType,
isMovie,
} from "./movie/schemas/movieCard";
export { movieCardSchema, movieCardsSchema, type MovieCardType, isMovie };
import { MovieService } from "./movie/movie";
export { MovieService };
import {
audiobookSchema,
audiobooksSchema,
audiobookCreateSchema,
type AudiobookType,
type AudiobookCreateType,
} from "./audiobook/schemas/audiobook";
export {
audiobookSchema,
audiobooksSchema,
audiobookCreateSchema,
type AudiobookType,
type AudiobookCreateType,
};
import {
audiobookCardSchema,
audiobookCardsSchema,
type AudiobookCardType,
isAudiobook,
} from "./audiobook/schemas/audiobookCard";
export {
audiobookCardSchema,
audiobookCardsSchema,
type AudiobookCardType,
isAudiobook,
};
import { AudiobookService } from "./audiobook/audiobook";
export { AudiobookService };
import { ItemService } from "./item";
export { ItemService };
import {
isSection,
type ItemType,
type ItemCardType,
type ItemCreateType,
type TypesOfItems,
type ItemSectionsType,
ItemSections,
} from "./types";
export {
isSection,
type ItemType,
type ItemCardType,
type ItemCreateType,
type TypesOfItems,
type ItemSectionsType,
ItemSections,
};

131
src/entities/item/item.ts Normal file
View File

@@ -0,0 +1,131 @@
import { ZodSchema } from "zod";
import { gameCreateSchema, gameSchema } from "./game/schemas/game";
import { movieCreateSchema, movieSchema } from "./movie/schemas/movie";
import { MovieService } from "./movie/movie";
import { HTTPService } from "@/shared/utils/http";
import { GameService } from "./game/game";
import {
audiobookCreateSchema,
audiobookSchema,
} from "./audiobook/schemas/audiobook";
import { AudiobookService } from "./audiobook/audiobook";
import {
IItemService,
ItemCardType,
ItemCreateType,
ItemPropertiesDescriptionType,
ItemSectionsType,
ItemType,
TypesOfItems,
UnionItemType,
} from "./types";
export abstract class ItemService {
private static get itemsConfiguration(): {
[k in TypesOfItems]: {
sectionUrl: ItemSectionsType;
formResolver: ZodSchema;
propertiesDescription: ItemPropertiesDescriptionType<UnionItemType>;
AddItem: (itemInfo: ItemCreateType) => Promise<ItemType | null>;
ChangeItem: (
id: number,
itemInfo: ItemCreateType
) => Promise<ItemType | null>;
};
} {
return {
[TypesOfItems.game]: {
sectionUrl: "games",
formResolver: gameCreateSchema,
propertiesDescription: GameService.propertiesDescription,
AddItem: async (itemInfo) =>
await HTTPService.post(`/games`, gameSchema, itemInfo),
ChangeItem: async (id: number, itemInfo) =>
await HTTPService.put(`/games/${id}`, gameSchema, itemInfo),
},
[TypesOfItems.movie]: {
sectionUrl: "movies",
formResolver: movieCreateSchema,
propertiesDescription: MovieService.propertiesDescription,
AddItem: async (itemInfo) =>
await HTTPService.post(`/movies`, movieSchema, itemInfo),
ChangeItem: async (id: number, itemInfo) =>
await HTTPService.put(`/movies/${id}`, movieSchema, itemInfo),
},
[TypesOfItems.audiobook]: {
sectionUrl: "audiobooks",
formResolver: audiobookCreateSchema,
propertiesDescription: AudiobookService.propertiesDescription,
AddItem: async (itemInfo) =>
await HTTPService.post(`/audiobooks`, audiobookSchema, itemInfo),
ChangeItem: async (id: number, itemInfo) =>
await HTTPService.put(`/audiobooks/${id}`, audiobookSchema, itemInfo),
},
};
}
static get itemSections(): {
[k in ItemSectionsType]: {
itemType: TypesOfItems;
popularSubsectionName: string;
sectionInviteText: string;
service: IItemService;
};
} {
return {
games: {
itemType: TypesOfItems.game,
popularSubsectionName: "Популярные игры",
sectionInviteText: 'Перейти в раздел "Игры"',
service: GameService,
},
movies: {
itemType: TypesOfItems.movie,
popularSubsectionName: "Популярные фильмы",
sectionInviteText: 'Перейти в раздел "Фильмы"',
service: MovieService,
},
audiobooks: {
itemType: TypesOfItems.audiobook,
popularSubsectionName: "Популярные аудиокниги",
sectionInviteText: 'Перейти в раздел "Аудиокниги"',
service: AudiobookService,
},
};
}
public static isExistingItem(
item: ItemCreateType | ItemType
): item is ItemType {
return (item as ItemType).id !== undefined;
}
public static GetFormResolver(
item: ItemCardType | ItemCreateType | ItemType
) {
return this.itemsConfiguration[item.type].formResolver;
}
public static GetSectionUrlByItemType(
item: ItemCardType | ItemCreateType | ItemType
) {
return this.itemsConfiguration[item.type].sectionUrl;
}
public static GetPropertiesDescriptionForItem<
T extends ItemType | ItemCreateType
>(item: T) {
return this.itemsConfiguration[item.type]
.propertiesDescription as ItemPropertiesDescriptionType<T>;
}
public static async AddItem(itemInfo: ItemCreateType) {
return await this.itemsConfiguration[itemInfo.type].AddItem(itemInfo);
}
public static async ChangeItem(id: number, itemInfo: ItemCreateType) {
return await this.itemsConfiguration[itemInfo.type].ChangeItem(
id,
itemInfo
);
}
}

View File

@@ -0,0 +1,65 @@
import { HTTPService } from "@/shared/utils/http";
import { movieCardsSchema } from "./schemas/movieCard";
import { MovieCreateType, movieSchema, MovieType } from "./schemas/movie";
import {
IItemService,
ItemCreateType,
ItemPropertiesDescriptionType,
ItemType,
staticImplements,
TypesOfItems,
} from "../types";
import { ItemService } from "../item";
@staticImplements<IItemService>()
export abstract class MovieService {
public static async GetCards() {
return await HTTPService.get("/movies/cards", movieCardsSchema);
}
public static async Get(id: number) {
return await HTTPService.get(`/movies/${id}`, movieSchema);
}
public static async Add(info: MovieCreateType) {
return await HTTPService.post(`/movies`, movieSchema, info);
}
public static async Change(id: number, info: MovieCreateType) {
return await HTTPService.put(`/movies/${id}`, movieSchema, info);
}
public static GetEmpty(): MovieCreateType {
return {
title: "",
torrent_file: "",
type: TypesOfItems.movie,
};
}
public static propertiesDescription: ItemPropertiesDescriptionType<MovieType> =
[
[
{ name: "Возраст", key: "age" },
{ name: "Язык", key: "language" },
{ name: "Субтитры", key: "subtitles" },
{
name: "Год выхода",
key: "release_date",
},
],
[
{
name: "Дата обновления раздачи",
key: "update_date",
value: (item: ItemType | ItemCreateType) => {
return ItemService.isExistingItem(item)
? item.update_date.toLocaleDateString("ru-ru")
: new Date().toLocaleDateString("ru-ru");
},
editable: false,
},
{ name: "Режисёр", key: "director" },
{ name: "Продолжительность", key: "duration" },
{ name: "Страна", key: "country" },
{ name: "Объём загрузки", key: "download_size" },
],
];
}

View File

@@ -0,0 +1,60 @@
import { z } from "zod";
import { movieCardBaseSchema } from "./movieCard";
export const movieBaseSchema = movieCardBaseSchema.merge(
z.object({
torrent_file: z.string().min(3, "У раздачи должен быть .torrent файл"),
trailer: z.string().optional(),
language: z.string().optional().nullable(),
subtitles: z.string().optional().nullable(),
download_size: z.string().optional().nullable(),
director: z.string().optional().nullable(),
duration: z.string().optional().nullable(),
country: z.string().optional().nullable(),
release_date: z
.string()
.optional()
.nullable()
.transform((d) =>
d
? new Date(d).toLocaleDateString("en-us", {
year: "numeric",
})
: undefined
),
})
);
export const movieCreateSchema = movieBaseSchema.merge(z.object({}));
export type MovieCreateType = z.infer<typeof movieCreateSchema>;
export const movieSchema = movieBaseSchema.merge(
z.object({
id: z.number().positive(),
owner_id: z.number().positive(),
update_date: z
.string()
.min(1)
.transform((d) => new Date(d)),
upload_date: z
.string()
.min(1)
.transform((d) => new Date(d)),
})
);
export type MovieType = z.infer<typeof movieSchema>;
export const isMovie = (a: any): a is MovieType => {
return movieSchema.safeParse(a).success;
};
export const moviesSchema = z.array(z.any()).transform((a) => {
const games: MovieType[] = [];
a.forEach((e) => {
if (isMovie(e)) games.push(movieSchema.parse(e));
else console.error("Movie parse error - ", e);
});
return games;
});

View File

@@ -0,0 +1,43 @@
import { z } from "zod";
import { TypesOfItems } from "../../types";
export const movieCardBaseSchema = z.object({
title: z.string().min(3, "Слишком короткое название"),
cover: z.string().optional().nullable(),
description: z.string().optional().nullable(),
age: z.string().optional().nullable(),
// Добавляем к каждому фильму поле, которое
// показывает, что item является фильмом
type: z
.any()
.optional()
.transform(() => TypesOfItems.movie),
});
export const movieCardSchema = movieCardBaseSchema.merge(
z.object({
id: z.number().positive(),
})
);
export type MovieCardType = z.infer<typeof movieCardSchema>;
export const isMovieCardStrict = (a: any): a is MovieCardType => {
return movieCardSchema.safeParse(a).success;
};
export const movieCardsSchema = z.array(z.any()).transform((a) => {
const cards: MovieCardType[] = [];
a.forEach((e) => {
if (isMovieCardStrict(e)) cards.push(movieCardSchema.parse(e));
else console.error("MovieCard parse error - ", e);
});
return cards;
});
export const isMovie = (a: any): a is MovieCardType => {
return (
movieCardBaseSchema.safeParse(a).success &&
(a as MovieCardType).type === TypesOfItems.movie
);
};

View File

@@ -0,0 +1,62 @@
import { GameCreateType, GameType } from "./game/schemas/game";
import { GameCardType } from "./game/schemas/gameCard";
import { MovieCreateType, MovieType } from "./movie/schemas/movie";
import { MovieCardType } from "./movie/schemas/movieCard";
import {
AudiobookCreateType,
AudiobookType,
} from "./audiobook/schemas/audiobook";
import { AudiobookCardType } from "./audiobook/schemas/audiobookCard";
export type ItemType = GameType | MovieType | AudiobookType;
export type ItemCardType = GameCardType | MovieCardType | AudiobookCardType;
export type ItemCreateType =
| GameCreateType
| MovieCreateType
| AudiobookCreateType;
export type UnionItemType = GameType & MovieType & AudiobookType;
export type UnionItemCardType = GameCardType &
MovieCardType &
AudiobookCardType;
export type UnionItemCreateType = GameCreateType &
MovieCreateType &
AudiobookCreateType;
export enum TypesOfItems {
game,
movie,
audiobook,
}
export type ItemSectionsType = "games" | "movies" | "audiobooks";
export const ItemSections = [
"games",
"movies",
"audiobooks",
] as ItemSectionsType[];
export const isSection = (a: string): a is ItemSectionsType => {
return (ItemSections as string[]).includes(a);
};
export type ItemPropertiesDescriptionType<T extends ItemType | ItemCreateType> =
{
name: string;
key: keyof T;
value?: (item: T) => string;
editable?: boolean;
}[][];
export interface IItemService {
GetCards(): Promise<ItemCardType[] | null>;
Get(id: number): Promise<ItemType | null>;
Add(info: ItemCreateType): Promise<ItemType | null>;
Change(id: number, info: ItemCreateType): Promise<ItemType | null>;
GetEmpty(): ItemCreateType;
propertiesDescription: ItemPropertiesDescriptionType<UnionItemType>;
}
export const staticImplements =
<T>() =>
<U extends T>(constructor: U) =>
constructor;