feat: search series search bar and small changes/additions

This commit is contained in:
FrenchGithubUser
2025-12-09 19:40:36 +01:00
parent 343aa39a68
commit e2a421294b
23 changed files with 787 additions and 29 deletions

View File

@@ -9,11 +9,11 @@ use utoipa::{
Modify, OpenApi,
};
use crate::handlers::search::search_title_group_tags_lite::SearchTitleGroupTagsLiteQuery;
use crate::handlers::{
search::search_torrent_requests::SearchTorrentRequestsQuery,
user_applications::get_user_applications::GetUserApplicationsQuery,
use crate::handlers::search::{
search_title_group_tags_lite::SearchTitleGroupTagsLiteQuery,
search_torrent_requests::SearchTorrentRequestsQuery,
};
use crate::handlers::user_applications::get_user_applications::GetUserApplicationsQuery;
#[derive(OpenApi)]
#[openapi(
@@ -52,6 +52,8 @@ use crate::handlers::{
crate::handlers::master_groups::create_master_group::exec,
crate::handlers::series::create_series::exec,
crate::handlers::series::get_series::exec,
crate::handlers::series::edit_series::exec,
crate::handlers::series::add_title_group::exec,
crate::handlers::subscriptions::create_subscription_forum_thread_posts::exec,
crate::handlers::subscriptions::remove_subscription_forum_thread_posts::exec,
crate::handlers::subscriptions::create_subscription_title_group_torrents::exec,
@@ -76,6 +78,7 @@ use crate::handlers::{
crate::handlers::search::search_collages::exec,
crate::handlers::search::search_collages_lite::exec,
crate::handlers::search::search_series::exec,
crate::handlers::search::search_series_lite::exec,
crate::handlers::search::search_forum::exec,
crate::handlers::torrent_requests::create_torrent_request::exec,
crate::handlers::torrent_requests::get_torrent_request::exec,

View File

@@ -3,6 +3,7 @@ pub mod search_collages;
pub mod search_collages_lite;
pub mod search_forum;
pub mod search_series;
pub mod search_series_lite;
pub mod search_title_group_info_lite;
pub mod search_title_group_tags;
pub mod search_title_group_tags_lite;
@@ -33,5 +34,6 @@ pub fn config<R: RedisPoolInterface + 'static>(cfg: &mut ServiceConfig) {
cfg.service(resource("/collages").route(get().to(self::search_collages::exec::<R>)));
cfg.service(resource("/collages/lite").route(get().to(self::search_collages_lite::exec::<R>)));
cfg.service(resource("/series").route(get().to(self::search_series::exec::<R>)));
cfg.service(resource("/series/lite").route(get().to(self::search_series_lite::exec::<R>)));
cfg.service(resource("/forum").route(get().to(self::search_forum::exec::<R>)));
}

View File

@@ -0,0 +1,34 @@
use crate::Arcadia;
use actix_web::{
web::{Data, Query},
HttpResponse,
};
use arcadia_common::error::Result;
use arcadia_storage::{models::series::SeriesLite, redis::RedisPoolInterface};
use serde::Deserialize;
use utoipa::{IntoParams, ToSchema};
#[derive(Debug, Deserialize, ToSchema, IntoParams)]
pub struct SearchSeriesLiteQuery {
pub name: String,
}
#[utoipa::path(
get,
operation_id = "Search series lite",
tag = "Search",
path = "/api/search/series/lite",
params (SearchSeriesLiteQuery),
description = "Case insensitive",
responses(
(status = 200, description = "Successfully got the series (lite)", body=Vec<SeriesLite>),
)
)]
pub async fn exec<R: RedisPoolInterface + 'static>(
query: Query<SearchSeriesLiteQuery>,
arc: Data<Arcadia<R>>,
) -> Result<HttpResponse> {
let series = arc.pool.search_series_lite(&query.name, 7).await?;
Ok(HttpResponse::Ok().json(series))
}

View File

@@ -0,0 +1,38 @@
use crate::Arcadia;
use actix_web::{
web::{Data, Json},
HttpResponse,
};
use arcadia_common::error::Result;
use arcadia_storage::{models::title_group::TitleGroup, redis::RedisPoolInterface};
use serde::Deserialize;
use utoipa::ToSchema;
#[derive(Debug, Deserialize, ToSchema)]
pub struct AddTitleGroupToSeriesRequest {
pub series_id: i64,
pub title_group_id: i32,
}
#[utoipa::path(
post,
operation_id = "Add title group to series",
tag = "Series",
path = "/api/series/title-group",
security(
("http" = ["Bearer"])
),
responses(
(status = 200, description = "Successfully attached the title group to the series", body=TitleGroup),
)
)]
pub async fn exec<R: RedisPoolInterface + 'static>(
form: Json<AddTitleGroupToSeriesRequest>,
arc: Data<Arcadia<R>>,
) -> Result<HttpResponse> {
arc.pool
.assign_title_group_to_series(form.title_group_id, form.series_id)
.await?;
Ok(HttpResponse::Ok().json(serde_json::json!({"result": "success"})))
}

View File

@@ -0,0 +1,41 @@
use crate::{middlewares::auth_middleware::Authdata, Arcadia};
use actix_web::{
web::{Data, Json},
HttpResponse,
};
use arcadia_common::error::{Error, Result};
use arcadia_storage::{
models::{
series::{EditedSeries, Series},
user::UserClass,
},
redis::RedisPoolInterface,
};
#[utoipa::path(
put,
operation_id = "Edit series",
tag = "Series",
path = "/api/series",
security(
("http" = ["Bearer"])
),
responses(
(status = 200, description = "Successfully edited the series", body=Series),
)
)]
pub async fn exec<R: RedisPoolInterface + 'static>(
form: Json<EditedSeries>,
arc: Data<Arcadia<R>>,
user: Authdata,
) -> Result<HttpResponse> {
let series = arc.pool.find_series(&form.id).await?;
if user.class != UserClass::Staff && series.created_by_id != user.sub {
return Err(Error::InsufficientPrivileges);
}
let updated_series = arc.pool.update_series(&form).await?;
Ok(HttpResponse::Ok().json(updated_series))
}

View File

@@ -1,13 +1,17 @@
pub mod add_title_group;
pub mod create_series;
pub mod edit_series;
pub mod get_series;
use actix_web::web::{get, post, resource, ServiceConfig};
use actix_web::web::{get, post, put, resource, ServiceConfig};
use arcadia_storage::redis::RedisPoolInterface;
pub fn config<R: RedisPoolInterface + 'static>(cfg: &mut ServiceConfig) {
cfg.service(
resource("")
.route(post().to(self::create_series::exec::<R>))
.route(get().to(self::get_series::exec::<R>)),
.route(get().to(self::get_series::exec::<R>))
.route(put().to(self::edit_series::exec::<R>)),
);
cfg.service(resource("/title-group").route(post().to(self::add_title_group::exec::<R>)));
}

View File

@@ -0,0 +1,15 @@
INSERT INTO
series (id, name, description, tags, covers, banners, created_by_id, created_at, updated_at)
VALUES
(
1,
'Test Series',
'A series used for testing',
'{test,series}',
'{https://example.com/cover.jpg}',
'{https://example.com/banner.jpg}',
1,
NOW(),
NOW()
);

View File

@@ -0,0 +1,88 @@
pub mod common;
pub mod mocks;
use std::sync::Arc;
use actix_web::{
http::StatusCode,
test::{self, call_service},
};
use arcadia_storage::{
connection_pool::ConnectionPool,
models::{
series::{EditedSeries, Series},
title_group::TitleGroupAndAssociatedData,
},
};
use mocks::mock_redis::MockRedisPool;
use sqlx::PgPool;
use crate::common::{
auth_header, call_and_read_body_json_with_status, create_test_app_and_login, TestUser,
};
#[sqlx::test(
fixtures("with_test_user2", "with_test_series"),
migrations = "../storage/migrations"
)]
async fn test_edit_series(pool: PgPool) {
let pool = Arc::new(ConnectionPool::with_pg_pool(pool));
let (service, user) =
create_test_app_and_login(pool, MockRedisPool::default(), 100, 100, TestUser::Staff).await;
let payload = EditedSeries {
id: 1,
name: "Updated Series".to_string(),
description: "Updated description".to_string(),
covers: vec!["https://example.com/updated-cover.jpg".to_string()],
banners: vec!["https://example.com/updated-banner.jpg".to_string()],
tags: vec!["updated".to_string()],
};
let req = test::TestRequest::put()
.uri("/api/series")
.insert_header(auth_header(&user.token))
.set_json(&payload)
.to_request();
let series: Series = call_and_read_body_json_with_status(&service, req, StatusCode::OK).await;
assert_eq!(series.name, payload.name);
assert_eq!(series.description, payload.description);
assert_eq!(series.covers, payload.covers);
assert_eq!(series.banners, Some(payload.banners));
assert_eq!(series.tags, payload.tags);
}
#[sqlx::test(
fixtures("with_test_user", "with_test_series", "with_test_title_group"),
migrations = "../storage/migrations"
)]
async fn test_add_title_group_to_series(pool: PgPool) {
let pool = Arc::new(ConnectionPool::with_pg_pool(pool));
let (service, user) =
create_test_app_and_login(pool, MockRedisPool::default(), 100, 100, TestUser::Standard)
.await;
let req = test::TestRequest::post()
.uri("/api/series/title-group")
.insert_header(auth_header(&user.token))
.set_json(serde_json::json!({
"series_id": 1,
"title_group_id": 1
}))
.to_request();
let _ = call_service(&service, req).await;
let req = test::TestRequest::get()
.uri("/api/title-groups?id=1")
.insert_header(auth_header(&user.token))
.to_request();
let title_group: TitleGroupAndAssociatedData =
call_and_read_body_json_with_status(&service, req, StatusCode::OK).await;
assert_eq!(title_group.title_group.id, 1);
assert_eq!(title_group.title_group.series_id, Some(1));
}

View File

@@ -108,6 +108,9 @@ pub enum Error {
#[error("could not create series")]
CouldNotCreateSeries(#[source] sqlx::Error),
#[error("could not update series")]
CouldNotUpdateSeries(#[source] sqlx::Error),
#[error("could not create api key")]
CouldNotCreateAPIKey(#[source] sqlx::Error),

View File

@@ -0,0 +1,75 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE series\n SET\n name = $2,\n description = $3,\n covers = $4,\n banners = $5,\n tags = $6,\n updated_at = NOW()\n WHERE id = $1\n RETURNING *\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 2,
"name": "description",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "tags",
"type_info": "TextArray"
},
{
"ordinal": 4,
"name": "covers",
"type_info": "TextArray"
},
{
"ordinal": 5,
"name": "banners",
"type_info": "TextArray"
},
{
"ordinal": 6,
"name": "created_by_id",
"type_info": "Int4"
},
{
"ordinal": 7,
"name": "created_at",
"type_info": "Timestamptz"
},
{
"ordinal": 8,
"name": "updated_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Int8",
"Varchar",
"Text",
"TextArray",
"TextArray",
"TextArray"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "61a6c11b6da252a26f963509c57cb726d29394f0e77d9d3e65f46b0284c96d78"
}

View File

@@ -0,0 +1,29 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n s.id,\n s.name\n FROM series s\n WHERE (s.name ILIKE '%' || $1 || '%')\n LIMIT $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Int8"
},
{
"ordinal": 1,
"name": "name",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Text",
"Int8"
]
},
"nullable": [
false,
false
]
},
"hash": "a47fbecaf179e762203deb5497de7d15ad7caaf68a0269a57b37dafba36121c3"
}

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE title_groups\n SET series_id = $2, updated_at = NOW()\n WHERE id = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4",
"Int8"
]
},
"nullable": []
},
"hash": "b70d718632b169f7a13e6c2a34e8aebe703b7b3b3e1d033807228c1f0d3e73de"
}

View File

@@ -29,6 +29,16 @@ pub struct UserCreatedSeries {
pub tags: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct EditedSeries {
pub id: i64,
pub name: String,
pub description: String,
pub covers: Vec<String>,
pub banners: Vec<String>,
pub tags: Vec<String>,
}
#[derive(Debug, Deserialize, Serialize, ToSchema)]
pub struct SeriesAndTitleGroupHierarchyLite {
pub series: Series,

View File

@@ -1,7 +1,8 @@
use crate::{
connection_pool::ConnectionPool,
models::series::{
SearchSeriesQuery, Series, SeriesSearchResponse, SeriesSearchResult, UserCreatedSeries,
EditedSeries, SearchSeriesQuery, Series, SeriesLite, SeriesSearchResponse,
SeriesSearchResult, UserCreatedSeries,
},
};
use arcadia_common::error::{Error, Result};
@@ -99,4 +100,57 @@ impl ConnectionPool {
total_items,
})
}
pub async fn search_series_lite(
&self,
name: &str,
results_amount: u8,
) -> Result<Vec<SeriesLite>> {
let results = sqlx::query_as!(
SeriesLite,
r#"
SELECT
s.id,
s.name
FROM series s
WHERE (s.name ILIKE '%' || $1 || '%')
LIMIT $2
"#,
name,
results_amount as i64
)
.fetch_all(self.borrow())
.await?;
Ok(results)
}
pub async fn update_series(&self, edited_series: &EditedSeries) -> Result<Series> {
let series = sqlx::query_as!(
Series,
r#"
UPDATE series
SET
name = $2,
description = $3,
covers = $4,
banners = $5,
tags = $6,
updated_at = NOW()
WHERE id = $1
RETURNING *
"#,
edited_series.id,
edited_series.name,
edited_series.description,
&edited_series.covers,
&edited_series.banners,
&edited_series.tags
)
.fetch_one(self.borrow())
.await
.map_err(Error::CouldNotUpdateSeries)?;
Ok(series)
}
}

View File

@@ -488,6 +488,27 @@ impl ConnectionPool {
Ok(updated_title_group)
}
pub async fn assign_title_group_to_series(
&self,
title_group_id: i32,
series_id: i64,
) -> Result<()> {
let _ = sqlx::query!(
r#"
UPDATE title_groups
SET series_id = $2, updated_at = NOW()
WHERE id = $1
"#,
title_group_id,
series_id
)
.fetch_one(self.borrow())
.await
.map_err(|e| Error::ErrorWhileUpdatingTitleGroup(e.to_string()))?;
Ok(())
}
pub async fn does_title_group_with_link_exist(
&self,
external_link: &str,

View File

@@ -23,5 +23,5 @@ Arcadia's frontend is a [SPA](https://developer.mozilla.org/en-US/docs/Glossary/
If you make changes to structs that are listed in the swagger or the api routes, you must regenerate the typescript interfaces with this command (from the frontend directory, while the backend is running):
```bash
npx openapi-generator-cli generate -g typescript-axios -i http://127.0.0.1:8080/swagger-json/openapi.json -o ./src/services/api-schema -t .openapi-generator/templates --config .openapi-generator/openapi-generator.config.json --global-property=models,apiDocs=false,modelDocs=false,skipFormModel=false
npx openapi-generator-cli generate -g typescript-axios -i http://127.0.0.1:8080/swagger-json/openapi.json -o ./src/services/api-schema -t .openapi-generator/templates --config .openapi-generator/openapi-generator.config.json --global-property=apiDocs=false,modelDocs=false,skipFormModel=false
```

View File

@@ -12,8 +12,8 @@
}
"
/>
<ArtistSearchBar :placeholder="t('artist.artist', 2)" @artistSelected="artistSelected" :clearInputOnSelect="true" v-model="searchForm.artists" />
<InputText type="text" :placeholder="t('series.series')" v-model="searchForm.series" size="small" />
<ArtistSearchBar :placeholder="t('artist.artist', 2)" :clickableSeriesLink="true" :clearInputOnSelect="true" v-model="searchForm.artists" />
<SeriesSearchBar :placeholder="t('series.series')" :clickableSeriesLink="true" :clearInputOnSelect="true" v-model="searchForm.series" />
<InputText type="text" :placeholder="t('forum.forum', 2)" v-model="searchForm.forums" size="small" />
<InputText type="text" :placeholder="t('user.user', 2)" v-model="searchForm.users" size="small" />
</div>
@@ -22,10 +22,10 @@
<script setup lang="ts">
import InputText from 'primevue/inputtext'
import ArtistSearchBar from './artist/ArtistSearchBar.vue'
import SeriesSearchBar from './series/SeriesSearchBar.vue'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import type { ArtistLite } from '@/services/api-schema'
const { t } = useI18n()
const router = useRouter()
@@ -38,10 +38,6 @@ const searchForm = ref({
forums: '',
users: '',
})
const artistSelected = (artist: ArtistLite) => {
router.push(`/artist/${artist.id}`)
}
</script>
<style scoped>

View File

@@ -10,7 +10,10 @@
@input="onInput"
>
<template #option="slotProps">
<div>{{ slotProps.option.name }}</div>
<RouterLink v-if="clickableSeriesLink" :to="`/artist/${slotProps.option.id}`" style="width: 100%">
{{ slotProps.option.name }}
</RouterLink>
<div v-else>{{ slotProps.option.name }}</div>
</template>
</AutoComplete>
</template>
@@ -24,6 +27,7 @@ const props = defineProps<{
placeholder: string
clearInputOnSelect: boolean
modelValue: string
clickableSeriesLink?: boolean
}>()
const emit = defineEmits<{

View File

@@ -0,0 +1,73 @@
<template>
<AutoComplete
v-model="name"
:suggestions="foundSeries"
@complete="search"
size="small"
:placeholder
optionLabel="name"
@option-select="seriesSelected"
@input="onInput"
>
<template #option="slotProps">
<RouterLink v-if="clickableSeriesLink" :to="`/series/${slotProps.option.id}`" style="width: 100%">
{{ slotProps.option.name }}
</RouterLink>
<div v-else>{{ slotProps.option.name }}</div>
</template>
</AutoComplete>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { AutoComplete, type AutoCompleteOptionSelectEvent } from 'primevue'
import { searchSeriesLite, type SeriesLite } from '@/services/api-schema'
import type { RouterLink } from 'vue-router'
const props = defineProps<{
placeholder: string
clearInputOnSelect: boolean
modelValue: string
clickableSeriesLink: boolean
}>()
const emit = defineEmits<{
'update:modelValue': [string]
seriesSelected: [SeriesLite]
}>()
const name = ref('')
watch(
() => props.modelValue,
(newValue) => {
name.value = newValue
},
{ immediate: true },
)
const foundSeries = ref<SeriesLite[]>()
const seriesSelected = (event: AutoCompleteOptionSelectEvent) => {
if (props.clearInputOnSelect) {
name.value = ''
}
const selectedSeriesName = (event.value as SeriesLite).name
emit('seriesSelected', event.value)
emit('update:modelValue', selectedSeriesName)
}
const onInput = () => {
emit('update:modelValue', name.value)
}
const search = () => {
if (name.value !== '') {
searchSeriesLite(name.value).then((series) => {
foundSeries.value = series
})
} else {
foundSeries.value = []
}
}
</script>

View File

@@ -23,6 +23,10 @@ import type { RequestArgs } from './base';
// @ts-ignore
import { BASE_PATH, COLLECTION_FORMATS, BaseAPI, RequiredError, operationServerMap } from './base';
export interface AddTitleGroupToSeriesRequest {
'series_id': number;
'title_group_id': number;
}
export interface AffiliatedArtistHierarchy {
'artist': Artist;
'artist_id': number;
@@ -332,6 +336,14 @@ export interface EditedCssSheet {
'old_name': string;
'preview_image_url': string;
}
export interface EditedSeries {
'banners': Array<string>;
'covers': Array<string>;
'description': string;
'id': number;
'name': string;
'tags': Array<string>;
}
export interface EditedTitleGroup {
'category'?: TitleGroupCategory | null;
'content_type': ContentType;
@@ -1516,6 +1528,7 @@ export interface TorrentSearch {
'order_by_direction': OrderByDirection;
'page': number;
'page_size': number;
'series_id'?: number | null;
'title_group_include_empty_groups': boolean;
'title_group_name'?: string | null;
'torrent_created_by_id'?: number | null;
@@ -5231,6 +5244,42 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
* Case insensitive
* @param {string} name
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
searchSeriesLite: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'name' is not null or undefined
assertParamExists('searchSeriesLite', 'name', name)
const localVarPath = `/api/search/series/lite`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
if (name !== undefined) {
localVarQueryParameter['name'] = name;
}
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@@ -5398,10 +5447,11 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio
* @param {number | null} [torrentSnatchedById]
* @param {number | null} [artistId]
* @param {number | null} [collageId]
* @param {number | null} [seriesId]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
searchTorrents: async (titleGroupIncludeEmptyGroups: boolean, page: number, pageSize: number, orderByColumn: TorrentSearchOrderByColumn, orderByDirection: OrderByDirection, titleGroupName?: string | null, torrentReported?: boolean | null, torrentStaffChecked?: boolean | null, torrentCreatedById?: number | null, torrentSnatchedById?: number | null, artistId?: number | null, collageId?: number | null, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
searchTorrents: async (titleGroupIncludeEmptyGroups: boolean, page: number, pageSize: number, orderByColumn: TorrentSearchOrderByColumn, orderByDirection: OrderByDirection, titleGroupName?: string | null, torrentReported?: boolean | null, torrentStaffChecked?: boolean | null, torrentCreatedById?: number | null, torrentSnatchedById?: number | null, artistId?: number | null, collageId?: number | null, seriesId?: number | null, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'titleGroupIncludeEmptyGroups' is not null or undefined
assertParamExists('searchTorrents', 'titleGroupIncludeEmptyGroups', titleGroupIncludeEmptyGroups)
// verify required parameter 'page' is not null or undefined
@@ -5456,6 +5506,10 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio
localVarQueryParameter['collage_id'] = collageId;
}
if (seriesId !== undefined) {
localVarQueryParameter['series_id'] = seriesId;
}
if (page !== undefined) {
localVarQueryParameter['page'] = page;
}
@@ -5561,6 +5615,18 @@ export const SearchApiFp = function(configuration?: Configuration) {
const localVarOperationServerBasePath = operationServerMap['SearchApi.searchSeries']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
* Case insensitive
* @param {string} name
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async searchSeriesLite(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<SeriesLite>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.searchSeriesLite(name, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['SearchApi.searchSeriesLite']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
* @param {string} name
@@ -5617,11 +5683,12 @@ export const SearchApiFp = function(configuration?: Configuration) {
* @param {number | null} [torrentSnatchedById]
* @param {number | null} [artistId]
* @param {number | null} [collageId]
* @param {number | null} [seriesId]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async searchTorrents(titleGroupIncludeEmptyGroups: boolean, page: number, pageSize: number, orderByColumn: TorrentSearchOrderByColumn, orderByDirection: OrderByDirection, titleGroupName?: string | null, torrentReported?: boolean | null, torrentStaffChecked?: boolean | null, torrentCreatedById?: number | null, torrentSnatchedById?: number | null, artistId?: number | null, collageId?: number | null, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PaginatedResultsTitleGroupHierarchyLite>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.searchTorrents(titleGroupIncludeEmptyGroups, page, pageSize, orderByColumn, orderByDirection, titleGroupName, torrentReported, torrentStaffChecked, torrentCreatedById, torrentSnatchedById, artistId, collageId, options);
async searchTorrents(titleGroupIncludeEmptyGroups: boolean, page: number, pageSize: number, orderByColumn: TorrentSearchOrderByColumn, orderByDirection: OrderByDirection, titleGroupName?: string | null, torrentReported?: boolean | null, torrentStaffChecked?: boolean | null, torrentCreatedById?: number | null, torrentSnatchedById?: number | null, artistId?: number | null, collageId?: number | null, seriesId?: number | null, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PaginatedResultsTitleGroupHierarchyLite>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.searchTorrents(titleGroupIncludeEmptyGroups, page, pageSize, orderByColumn, orderByDirection, titleGroupName, torrentReported, torrentStaffChecked, torrentCreatedById, torrentSnatchedById, artistId, collageId, seriesId, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['SearchApi.searchTorrents']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
@@ -5689,6 +5756,15 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat
searchSeries(page: number, pageSize: number, name?: string | null, tags?: Array<string> | null, options?: RawAxiosRequestConfig): AxiosPromise<SeriesSearchResponse> {
return localVarFp.searchSeries(page, pageSize, name, tags, options).then((request) => request(axios, basePath));
},
/**
* Case insensitive
* @param {string} name
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
searchSeriesLite(name: string, options?: RawAxiosRequestConfig): AxiosPromise<Array<SeriesLite>> {
return localVarFp.searchSeriesLite(name, options).then((request) => request(axios, basePath));
},
/**
*
* @param {string} name
@@ -5736,11 +5812,12 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat
* @param {number | null} [torrentSnatchedById]
* @param {number | null} [artistId]
* @param {number | null} [collageId]
* @param {number | null} [seriesId]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
searchTorrents(titleGroupIncludeEmptyGroups: boolean, page: number, pageSize: number, orderByColumn: TorrentSearchOrderByColumn, orderByDirection: OrderByDirection, titleGroupName?: string | null, torrentReported?: boolean | null, torrentStaffChecked?: boolean | null, torrentCreatedById?: number | null, torrentSnatchedById?: number | null, artistId?: number | null, collageId?: number | null, options?: RawAxiosRequestConfig): AxiosPromise<PaginatedResultsTitleGroupHierarchyLite> {
return localVarFp.searchTorrents(titleGroupIncludeEmptyGroups, page, pageSize, orderByColumn, orderByDirection, titleGroupName, torrentReported, torrentStaffChecked, torrentCreatedById, torrentSnatchedById, artistId, collageId, options).then((request) => request(axios, basePath));
searchTorrents(titleGroupIncludeEmptyGroups: boolean, page: number, pageSize: number, orderByColumn: TorrentSearchOrderByColumn, orderByDirection: OrderByDirection, titleGroupName?: string | null, torrentReported?: boolean | null, torrentStaffChecked?: boolean | null, torrentCreatedById?: number | null, torrentSnatchedById?: number | null, artistId?: number | null, collageId?: number | null, seriesId?: number | null, options?: RawAxiosRequestConfig): AxiosPromise<PaginatedResultsTitleGroupHierarchyLite> {
return localVarFp.searchTorrents(titleGroupIncludeEmptyGroups, page, pageSize, orderByColumn, orderByDirection, titleGroupName, torrentReported, torrentStaffChecked, torrentCreatedById, torrentSnatchedById, artistId, collageId, seriesId, options).then((request) => request(axios, basePath));
},
};
};
@@ -5808,6 +5885,16 @@ export class SearchApi extends BaseAPI {
return SearchApiFp(this.configuration).searchSeries(page, pageSize, name, tags, options).then((request) => request(this.axios, this.basePath));
}
/**
* Case insensitive
* @param {string} name
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
public searchSeriesLite(name: string, options?: RawAxiosRequestConfig) {
return SearchApiFp(this.configuration).searchSeriesLite(name, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {string} name
@@ -5858,11 +5945,12 @@ export class SearchApi extends BaseAPI {
* @param {number | null} [torrentSnatchedById]
* @param {number | null} [artistId]
* @param {number | null} [collageId]
* @param {number | null} [seriesId]
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
public searchTorrents(titleGroupIncludeEmptyGroups: boolean, page: number, pageSize: number, orderByColumn: TorrentSearchOrderByColumn, orderByDirection: OrderByDirection, titleGroupName?: string | null, torrentReported?: boolean | null, torrentStaffChecked?: boolean | null, torrentCreatedById?: number | null, torrentSnatchedById?: number | null, artistId?: number | null, collageId?: number | null, options?: RawAxiosRequestConfig) {
return SearchApiFp(this.configuration).searchTorrents(titleGroupIncludeEmptyGroups, page, pageSize, orderByColumn, orderByDirection, titleGroupName, torrentReported, torrentStaffChecked, torrentCreatedById, torrentSnatchedById, artistId, collageId, options).then((request) => request(this.axios, this.basePath));
public searchTorrents(titleGroupIncludeEmptyGroups: boolean, page: number, pageSize: number, orderByColumn: TorrentSearchOrderByColumn, orderByDirection: OrderByDirection, titleGroupName?: string | null, torrentReported?: boolean | null, torrentStaffChecked?: boolean | null, torrentCreatedById?: number | null, torrentSnatchedById?: number | null, artistId?: number | null, collageId?: number | null, seriesId?: number | null, options?: RawAxiosRequestConfig) {
return SearchApiFp(this.configuration).searchTorrents(titleGroupIncludeEmptyGroups, page, pageSize, orderByColumn, orderByDirection, titleGroupName, torrentReported, torrentStaffChecked, torrentCreatedById, torrentSnatchedById, artistId, collageId, seriesId, options).then((request) => request(this.axios, this.basePath));
}
}
@@ -5938,6 +6026,12 @@ export const searchSeries = async (requestParameters: SearchSeriesRequest, optio
return response.data;
};
export const searchSeriesLite = async (name: string, options?: RawAxiosRequestConfig): Promise<Array<SeriesLite>> => {
const response = await searchApi.searchSeriesLite(name, options);
return response.data;
};
export interface SearchTitleGroupInfoRequest {
/** */
'name': string;
@@ -6008,11 +6102,13 @@ export interface SearchTorrentsRequest {
'artist_id'?: number | null;
/** */
'collage_id'?: number | null;
/** */
'series_id'?: number | null;
}
export const searchTorrents = async (requestParameters: SearchTorrentsRequest, options?: RawAxiosRequestConfig): Promise<PaginatedResultsTitleGroupHierarchyLite> => {
const response = await searchApi.searchTorrents(requestParameters['title_group_include_empty_groups']!, requestParameters['page']!, requestParameters['page_size']!, requestParameters['order_by_column']!, requestParameters['order_by_direction']!, requestParameters['title_group_name']!, requestParameters['torrent_reported']!, requestParameters['torrent_staff_checked']!, requestParameters['torrent_created_by_id']!, requestParameters['torrent_snatched_by_id']!, requestParameters['artist_id']!, requestParameters['collage_id']!, options);
const response = await searchApi.searchTorrents(requestParameters['title_group_include_empty_groups']!, requestParameters['page']!, requestParameters['page_size']!, requestParameters['order_by_column']!, requestParameters['order_by_direction']!, requestParameters['title_group_name']!, requestParameters['torrent_reported']!, requestParameters['torrent_staff_checked']!, requestParameters['torrent_created_by_id']!, requestParameters['torrent_snatched_by_id']!, requestParameters['artist_id']!, requestParameters['collage_id']!, requestParameters['series_id']!, options);
return response.data;
};
@@ -6022,6 +6118,45 @@ export const searchTorrents = async (requestParameters: SearchTorrentsRequest, o
*/
export const SeriesApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
*
* @param {AddTitleGroupToSeriesRequest} addTitleGroupToSeriesRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
addTitleGroupToSeries: async (addTitleGroupToSeriesRequest: AddTitleGroupToSeriesRequest, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'addTitleGroupToSeriesRequest' is not null or undefined
assertParamExists('addTitleGroupToSeries', 'addTitleGroupToSeriesRequest', addTitleGroupToSeriesRequest)
const localVarPath = `/api/series/title-group`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication http required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(addTitleGroupToSeriesRequest, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {UserCreatedSeries} userCreatedSeries
@@ -6061,6 +6196,45 @@ export const SeriesApiAxiosParamCreator = function (configuration?: Configuratio
options: localVarRequestOptions,
};
},
/**
*
* @param {EditedSeries} editedSeries
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
editSeries: async (editedSeries: EditedSeries, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'editedSeries' is not null or undefined
assertParamExists('editSeries', 'editedSeries', editedSeries)
const localVarPath = `/api/series`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication http required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(editedSeries, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {number} id
@@ -6106,6 +6280,18 @@ export const SeriesApiAxiosParamCreator = function (configuration?: Configuratio
export const SeriesApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = SeriesApiAxiosParamCreator(configuration)
return {
/**
*
* @param {AddTitleGroupToSeriesRequest} addTitleGroupToSeriesRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async addTitleGroupToSeries(addTitleGroupToSeriesRequest: AddTitleGroupToSeriesRequest, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<TitleGroup>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.addTitleGroupToSeries(addTitleGroupToSeriesRequest, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['SeriesApi.addTitleGroupToSeries']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
* @param {UserCreatedSeries} userCreatedSeries
@@ -6118,6 +6304,18 @@ export const SeriesApiFp = function(configuration?: Configuration) {
const localVarOperationServerBasePath = operationServerMap['SeriesApi.createSeries']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
* @param {EditedSeries} editedSeries
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async editSeries(editedSeries: EditedSeries, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Series>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.editSeries(editedSeries, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['SeriesApi.editSeries']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
*
* @param {number} id
@@ -6139,6 +6337,15 @@ export const SeriesApiFp = function(configuration?: Configuration) {
export const SeriesApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = SeriesApiFp(configuration)
return {
/**
*
* @param {AddTitleGroupToSeriesRequest} addTitleGroupToSeriesRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
addTitleGroupToSeries(addTitleGroupToSeriesRequest: AddTitleGroupToSeriesRequest, options?: RawAxiosRequestConfig): AxiosPromise<TitleGroup> {
return localVarFp.addTitleGroupToSeries(addTitleGroupToSeriesRequest, options).then((request) => request(axios, basePath));
},
/**
*
* @param {UserCreatedSeries} userCreatedSeries
@@ -6148,6 +6355,15 @@ export const SeriesApiFactory = function (configuration?: Configuration, basePat
createSeries(userCreatedSeries: UserCreatedSeries, options?: RawAxiosRequestConfig): AxiosPromise<Series> {
return localVarFp.createSeries(userCreatedSeries, options).then((request) => request(axios, basePath));
},
/**
*
* @param {EditedSeries} editedSeries
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
editSeries(editedSeries: EditedSeries, options?: RawAxiosRequestConfig): AxiosPromise<Series> {
return localVarFp.editSeries(editedSeries, options).then((request) => request(axios, basePath));
},
/**
*
* @param {number} id
@@ -6164,6 +6380,16 @@ export const SeriesApiFactory = function (configuration?: Configuration, basePat
* SeriesApi - object-oriented interface
*/
export class SeriesApi extends BaseAPI {
/**
*
* @param {AddTitleGroupToSeriesRequest} addTitleGroupToSeriesRequest
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
public addTitleGroupToSeries(addTitleGroupToSeriesRequest: AddTitleGroupToSeriesRequest, options?: RawAxiosRequestConfig) {
return SeriesApiFp(this.configuration).addTitleGroupToSeries(addTitleGroupToSeriesRequest, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {UserCreatedSeries} userCreatedSeries
@@ -6174,6 +6400,16 @@ export class SeriesApi extends BaseAPI {
return SeriesApiFp(this.configuration).createSeries(userCreatedSeries, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {EditedSeries} editedSeries
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
public editSeries(editedSeries: EditedSeries, options?: RawAxiosRequestConfig) {
return SeriesApiFp(this.configuration).editSeries(editedSeries, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {number} id
@@ -6190,12 +6426,24 @@ export const seriesApi = new SeriesApi(undefined, undefined, globalAxios);
export const addTitleGroupToSeries = async (addTitleGroupToSeriesRequest: AddTitleGroupToSeriesRequest, options?: RawAxiosRequestConfig): Promise<TitleGroup> => {
const response = await seriesApi.addTitleGroupToSeries(addTitleGroupToSeriesRequest, options);
return response.data;
};
export const createSeries = async (userCreatedSeries: UserCreatedSeries, options?: RawAxiosRequestConfig): Promise<Series> => {
const response = await seriesApi.createSeries(userCreatedSeries, options);
return response.data;
};
export const editSeries = async (editedSeries: EditedSeries, options?: RawAxiosRequestConfig): Promise<Series> => {
const response = await seriesApi.editSeries(editedSeries, options);
return response.data;
};
export const getSeries = async (id: number, options?: RawAxiosRequestConfig): Promise<SeriesAndTitleGroupHierarchyLite> => {
const response = await seriesApi.getSeries(id, options);
return response.data;

View File

@@ -115,7 +115,7 @@ export class Configuration {
* @return True if the given MIME is JSON, false otherwise.
*/
public isJsonMime(mime: string): boolean {
const jsonMime: RegExp = new RegExp('^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$', 'i');
const jsonMime: RegExp = new RegExp('^(application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(;.*)?$', 'i');
return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json');
}
}

View File

@@ -15,4 +15,3 @@
export * from "./api";
export * from "./configuration";

View File

@@ -16,7 +16,7 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, watch } from 'vue'
import { useRoute } from 'vue-router'
import SeriesSlimHeader from '@/components/series/SeriesSlimHeader.vue'
import ContentContainer from '@/components/ContentContainer.vue'
@@ -32,7 +32,7 @@ const title_groups = ref<TitleGroupHierarchyLite[]>([])
const title_group_preview_mode = ref<'table' | 'cover-only'>('table') // TODO: make a select button to switch from cover-only to table
const siteName = import.meta.env.VITE_SITE_NAME
onMounted(async () => {
const fetchSeries = async () => {
const id = Number(route.params.id)
// TODO: either toast an error message + redirect or show an error component
if (!Number.isNaN(id)) {
@@ -42,7 +42,13 @@ onMounted(async () => {
}
document.title = `${series.value?.name} - ${siteName}`
}
onMounted(async () => {
fetchSeries()
})
watch(() => route.params.id, fetchSeries, { immediate: true })
</script>
<style scoped>