add comic artists

This commit is contained in:
Thomas Peetz
2025-09-21 20:09:46 +02:00
parent 64ed4876a5
commit 6d88b87f93
30 changed files with 366 additions and 107 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 B

@@ -1,5 +1,4 @@
import { Component, DestroyRef, inject, OnInit, signal } from '@angular/core'; import { Component, DestroyRef, inject, OnInit, signal } from '@angular/core';
import { RouterLink, RouterLinkActive } from '@angular/router';
import { Artist } from '../comic-artists/artist.model'; import { Artist } from '../comic-artists/artist.model';
import { ComicArtistComponent } from '../comic-artist/comic-artist.component'; import { ComicArtistComponent } from '../comic-artist/comic-artist.component';
import { ArtistService } from '../comic-artists/artist.service'; import { ArtistService } from '../comic-artists/artist.service';
@@ -2,3 +2,9 @@ export interface Artist {
id: string; id: string;
name: string; name: string;
} }
export interface ArtistDetails {
id: string;
name: string;
weblink: string;
}
@@ -2,7 +2,7 @@ import { inject, Injectable, signal } from "@angular/core";
import { HttpClient } from "@angular/common/http"; import { HttpClient } from "@angular/common/http";
import { catchError, map, throwError } from "rxjs"; import { catchError, map, throwError } from "rxjs";
import { ErrorService } from "../../../shared/error.service"; import { ErrorService } from "../../../shared/error.service";
import { Artist } from "./artist.model"; import { Artist, ArtistDetails } from "./artist.model";
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@@ -18,6 +18,10 @@ export class ArtistService {
return this.fetchArtists('http://127.0.0.1:8800/api/comics/artists', 'Someting went wrong fetching artists. Please try again later-'); return this.fetchArtists('http://127.0.0.1:8800/api/comics/artists', 'Someting went wrong fetching artists. Please try again later-');
} }
loadArtistDetails(artistId: string | null) {
return this.fetchArtistDetails('http://127.0.0.1:8800/api/comics/artists/' + artistId, 'Someting went wrong fetching comic artists. Please try again later.');
}
private fetchArtists(url: string, errorMessage: string) { private fetchArtists(url: string, errorMessage: string) {
return this.httpClient.get<Artist[]>(url).pipe( return this.httpClient.get<Artist[]>(url).pipe(
map((resData) => resData), map((resData) => resData),
@@ -27,4 +31,17 @@ export class ArtistService {
}) })
); );
} }
private fetchArtistDetails(url: string, errorMessage: string) {
return this.httpClient.get<ArtistDetails>(url).pipe(
map((resData) => {
console.log(resData);
return resData;
}),
catchError((error) => {
console.log(error);
return throwError(() => new Error(errorMessage));
})
);
}
} }
@@ -4,6 +4,11 @@
<app-comic-artists-list /> <app-comic-artists-list />
</div> </div>
<div> <div>
@if (artist()) {
<h2>{{ artist().name }}</h2>
<a href="{{ artist().weblink }}">{{ artist().name }}</a>
} @else {
<h2>Artist Details</h2> <h2>Artist Details</h2>
}
</div> </div>
</div> </div>
@@ -1,5 +1,8 @@
import { Component } from '@angular/core'; import { Component, inject, input } from '@angular/core';
import { ComicArtistsListComponent } from '../comic-artists-list/comic-artists-list.component'; import { ComicArtistsListComponent } from '../comic-artists-list/comic-artists-list.component';
import { ArtistDetails } from './artist.model';
import { ActivatedRouteSnapshot, ResolveFn, RouterStateSnapshot } from '@angular/router';
import { ArtistService } from './artist.service';
@Component({ @Component({
selector: 'app-comic-artists', selector: 'app-comic-artists',
@@ -8,5 +11,13 @@ import { ComicArtistsListComponent } from '../comic-artists-list/comic-artists-l
styleUrl: './comic-artists.component.css' styleUrl: './comic-artists.component.css'
}) })
export class ComicArtistsComponent { export class ComicArtistsComponent {
artist = input.required<ArtistDetails>();
} }
export const artistResolver: ResolveFn<ArtistDetails> = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
const artistService = inject(ArtistService);
const artistId = route.paramMap.get('artistId');
const artistDetails = artistService.loadArtistDetails(artistId);
console.log(artistDetails);
return artistDetails;
};
@@ -1,5 +1,5 @@
import { Routes } from "@angular/router"; import { Routes } from "@angular/router";
import { ComicArtistsComponent } from "../comic-artists/comic-artists.component"; import { artistResolver, ComicArtistsComponent } from "../comic-artists/comic-artists.component";
import { ComicPublishersComponent } from './../comic-publishers/comic-publishers.component'; import { ComicPublishersComponent } from './../comic-publishers/comic-publishers.component';
import { ComicComicsComponent, comicResolver } from "../comic-comics/comic-comics.component"; import { ComicComicsComponent, comicResolver } from "../comic-comics/comic-comics.component";
@@ -19,10 +19,10 @@ export const comicRoutes: Routes = [
path: 'publisher', path: 'publisher',
component: ComicPublishersComponent component: ComicPublishersComponent
}, },
// { {
// path: 'publishers/:publisherId', path: 'publishers/:publisherId',
// component: PublishersComponent, component: ComicPublishersComponent,
// }, },
{ {
path: 'artist', path: 'artist',
component: ComicArtistsComponent component: ComicArtistsComponent
@@ -30,5 +30,8 @@ export const comicRoutes: Routes = [
{ {
path: 'artist/:artistId', path: 'artist/:artistId',
component: ComicArtistsComponent, component: ComicArtistsComponent,
resolve: {
artist: artistResolver
}
}, },
]; ];
@@ -0,0 +1,17 @@
<div>
<article>
<a [routerLink]="['/', 'media', 'file', mediafile().id]" routerLinkActive="active">
<span>{{ mediafile().title }}</span>
</a>
<a href="{{ mediafile().url }}"> &gt;&gt;</a>
<p>
<span>
@if (mediafile().review) {
Review<img class="images" src="tick.png" />
} @else {
Review<img class="images" src="cross.png" />
}
</span>
</p>
</article>
</div>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MediaFileComponent } from './media-file.component';
describe('MediaFileComponent', () => {
let component: MediaFileComponent;
let fixture: ComponentFixture<MediaFileComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MediaFileComponent]
})
.compileComponents();
fixture = TestBed.createComponent(MediaFileComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,13 @@
import { Component, input } from '@angular/core';
import { MediaFile } from '../media-files/media-file.model';
import { RouterLink, RouterLinkActive } from '@angular/router';
@Component({
selector: 'app-media-file',
imports: [RouterLink, RouterLinkActive],
templateUrl: './media-file.component.html',
styleUrl: './media-file.component.css'
})
export class MediaFileComponent {
mediafile = input.required<MediaFile>();
}
@@ -0,0 +1,14 @@
ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
gap: 0.5rem;
overflow: auto;
}
@media (min-width: 768px) {
ul {
flex-direction: column;
}
}
@@ -0,0 +1,7 @@
<ul>
@for (mediafile of files(); track mediafile.id) {
<li>
<app-media-file [mediafile]="mediafile" />
</li>
}
</ul>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MediaFilesListComponent } from './media-files-list.component';
describe('MediaFilesListComponent', () => {
let component: MediaFilesListComponent;
let fixture: ComponentFixture<MediaFilesListComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MediaFilesListComponent]
})
.compileComponents();
fixture = TestBed.createComponent(MediaFilesListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,37 @@
import { Component, DestroyRef, inject, OnInit, signal } from '@angular/core';
import { MediaFileComponent } from '../media-file/media-file.component';
import { MediaFileService } from '../media-files/media-file.service';
import { MediaFile } from '../media-files/media-file.model';
@Component({
selector: 'app-media-files-list',
imports: [MediaFileComponent],
templateUrl: './media-files-list.component.html',
styleUrl: './media-files-list.component.css'
})
export class MediaFilesListComponent implements OnInit {
files = signal<MediaFile[]>([]);
isFetching = signal(false);
error = signal('');
private mediaFileService = inject(MediaFileService);
private destroyRef = inject(DestroyRef);
ngOnInit() {
this.isFetching.set(true);
const subscription = this.mediaFileService.loadFiles().subscribe({
next: (files) => {
this.files.set(files);
},
error: (error: Error) => {
this.error.set(error.message);
},
complete: () => {
this.isFetching.set(false);
},
});
this.destroyRef.onDestroy(() => {
subscription.unsubscribe();
});
}
}
@@ -0,0 +1,11 @@
import { StreamingResourceOptions } from "@angular/core";
export interface MediaFile {
id: string;
title: string;
file_name: string;
cloud_link: string;
url: string;
review: boolean;
should_download: boolean;
}
@@ -0,0 +1,30 @@
import { inject, Injectable, signal } from "@angular/core";
import { ErrorService } from "../../../shared/error.service";
import { HttpClient } from "@angular/common/http";
import { MediaFile } from "./media-file.model";
import { catchError, map, throwError } from "rxjs";
@Injectable({
providedIn: 'root',
})
export class MediaFileService {
private errorService = inject(ErrorService);
private httpClient = inject(HttpClient);
private files = signal<MediaFile[]>([]);
loadedFiles = this.files.asReadonly();
loadFiles() {
return this.fetchMediaFiles('http://127.0.0.1:8800/api/media/files', 'Someting went wrong fetching artists. Please try again later-');
}
private fetchMediaFiles(url: string, errorMessage: string) {
return this.httpClient.get<MediaFile[]>(url).pipe(
map((resData) => resData),
catchError((error) => {
console.log(error);
return throwError(() => new Error(errorMessage));
})
);
}
}
@@ -0,0 +1,5 @@
.grid-container {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 20px;
}
@@ -1 +1,7 @@
<p>media-files works!</p> <div class="grid-container">
<div>
<app-media-files-list />
</div>
<div>
</div>
</div>
@@ -1,8 +1,9 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { MediaFilesListComponent } from '../media-files-list/media-files-list.component';
@Component({ @Component({
selector: 'app-media-files', selector: 'app-media-files',
imports: [], imports: [MediaFilesListComponent],
templateUrl: './media-files.component.html', templateUrl: './media-files.component.html',
styleUrl: './media-files.component.css' styleUrl: './media-files.component.css'
}) })
@@ -8,12 +8,24 @@ export const mediaRoutes: Routes = [
path: 'file', path: 'file',
component: MediaFilesComponent component: MediaFilesComponent
}, },
{
path: 'file/:fileId',
component: MediaFilesComponent,
},
{ {
path: 'actor', path: 'actor',
component: MediaActorsComponent component: MediaActorsComponent
}, },
{
path: 'actor/:actorId',
component: MediaActorsComponent,
},
{ {
path: 'video', path: 'video',
component: MediaVideosComponent component: MediaVideosComponent
}, },
{
path: 'video/:videoId',
component: MediaVideosComponent,
},
]; ];
+8 -7
View File
@@ -1,13 +1,15 @@
from typing import List, AnyStr from typing import List
from fastapi import APIRouter, HTTPException, status from fastapi import APIRouter, HTTPException, status
from src.apis.utils import SessionDep from src.apis.utils import SessionDep
from src.core.log_conf import logger from src.core.log_conf import logger
from src.db.repository.comics.artist import get_artist_details from src.db.repository.comics.artist import get_artist_details
from src.db.repository.comics.comic import list_comics, get_issue_details from src.db.repository.comics.comic import get_comic_details, get_short_info, list_comics, get_issue_details
from src.schema.comics.comic import ComicResponse, ComicDetailsResponse, get_comic_details, get_short_info from src.schema.comics.artist_details import ArtistDetailResponse
from src.schema.comics.artist import ArtistCreation, ArtistDetailResponse, ArtistResponse from src.schema.comics.comic import ComicResponse
from src.schema.comics.artist import ArtistCreation, ArtistResponse
from src.db.models.comic import Comic, Artist, Issue from src.db.models.comic import Comic, Artist, Issue
from src.schema.comics.comic_details import ComicDetailsResponse
from src.schema.comics.issue import IssueDetailsResponse from src.schema.comics.issue import IssueDetailsResponse
router = APIRouter() router = APIRouter()
@@ -23,7 +25,7 @@ def get_all_comics(db: SessionDep) -> List[ComicResponse]:
return results return results
@router.get("/comics/{comic_id}", response_model=ComicDetailsResponse) @router.get("/comics/{comic_id}", response_model=ComicDetailsResponse)
def get_comic(comic_id: AnyStr, db: SessionDep) -> ComicDetailsResponse: def get_comic(comic_id: str, db: SessionDep) -> ComicDetailsResponse:
comic = db.get(Comic, comic_id) comic = db.get(Comic, comic_id)
if comic is None: if comic is None:
raise HTTPException(status_code=404, detail="Comic could not be found") raise HTTPException(status_code=404, detail="Comic could not be found")
@@ -41,7 +43,7 @@ def get_all_artists(db: SessionDep) -> List[ArtistResponse]:
return results return results
@router.get("/artists/{artist_id}", response_model=ArtistDetailResponse) @router.get("/artists/{artist_id}", response_model=ArtistDetailResponse)
def get_artist(artist_id: AnyStr, db: SessionDep) -> ArtistDetailResponse: def get_artist(artist_id: str, db: SessionDep) -> ArtistDetailResponse:
artist = db.get(Artist, artist_id) artist = db.get(Artist, artist_id)
if artist is None: if artist is None:
raise HTTPException(status_code=404, detail="Artist could not be found") raise HTTPException(status_code=404, detail="Artist could not be found")
@@ -67,4 +69,3 @@ def get_issues(db: SessionDep) -> List[IssueDetailsResponse]:
for issue in issues: for issue in issues:
results.append(get_issue_details(issue)) results.append(get_issue_details(issue))
return results return results
+17 -7
View File
@@ -1,19 +1,29 @@
from typing import List
from src.db.models.comic import Artist from src.db.models.comic import Artist
from src.schema.comics.artist import ArtistDetailResponse from src.schema.comics.artist_details import ArtistDetailResponse, ArtistWorktypeComicResponse
from src.schema.comics.comic import ComicResponse
from src.schema.comics.worktype import WorktypeResponse
def get_artist_details(artist: Artist) -> ArtistDetailResponse: def get_artist_details(artist: Artist) -> ArtistDetailResponse:
works = {} works: List[ArtistWorktypeComicResponse] = []
works_map = {}
for work in artist.comic_works: for work in artist.comic_works:
work_type = work.work_type.name worktype_id = work.work_type.id
comic_title = work.comic.title if worktype_id in works_map:
if work_type in works: comic = ComicResponse(id=work.comic.id, title=work.comic.title, completed=work.comic.completed)
works[work_type].append(comic_title) works_map[worktype_id].comics.append(comic)
else: else:
works[work_type] = [comic_title] works_map[worktype_id] = ArtistWorktypeComicResponse(
worktype=WorktypeResponse(id=worktype_id, name=work.work_type.name),
comics=[ComicResponse(id=work.comic.id, title=work.comic.title, completed=work.comic.completed)]
)
for value in works_map.values():
works.append(value)
response = ArtistDetailResponse( response = ArtistDetailResponse(
id=artist.id, id=artist.id,
name=artist.name, name=artist.name,
weblink=artist.weblink,
works=works works=works
) )
return response return response
+48 -3
View File
@@ -1,11 +1,15 @@
from typing import List, Type, AnyStr from typing import Dict, List
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from src.core.log_conf import logger from src.core.log_conf import logger
from src.db.models.comic import Comic, Issue from src.db.models.comic import Comic, Issue
from src.schema.comics.comic import ComicSchema from src.schema.comics.artist import ArtistResponse
from src.schema.comics.comic import ComicResponse, ComicSchema
from src.schema.comics.comic_details import ComicDetailsResponse, ComicWorktypeArtistResponse
from src.schema.comics.issue import IssueDetailsResponse from src.schema.comics.issue import IssueDetailsResponse
from src.schema.comics.volume import VolumeResponse
from src.schema.comics.worktype import WorktypeResponse
def list_comics(db: Session) -> List[Comic]: def list_comics(db: Session) -> List[Comic]:
@@ -25,7 +29,48 @@ def get_issue_details(issue: Issue) -> IssueDetailsResponse:
return response return response
def update_comic(comic: ComicSchema, comic_id: AnyStr, db: Session) -> type[Comic] | None: def update_comic(comic: ComicSchema, comic_id: str, db: Session) -> type[Comic] | None:
logger.info(f"update_comic: {comic} with {comic_id}") logger.info(f"update_comic: {comic} with {comic_id}")
comic = db.get(Comic, comic_id) # type: ignore comic = db.get(Comic, comic_id) # type: ignore
return comic # type: ignore return comic # type: ignore
def get_short_info(comic: Comic) -> ComicResponse:
response = ComicResponse(
id=comic.id,
title=str(comic.title),
completed=bool(comic.completed == 1)
)
return response
def get_comic_details(comic: Comic) -> ComicDetailsResponse:
volumes: List[VolumeResponse] = []
for volume in comic.volumes:
volumes.append(VolumeResponse(id=volume.id, name=volume.name))
works: List[ComicWorktypeArtistResponse] = []
works_map: Dict[str, ComicWorktypeArtistResponse] = {}
for work in comic.comic_works:
worktype_id = work.work_type.id
if worktype_id in works_map:
artist = ArtistResponse(id=work.artist.id, name=work.artist.name)
works_map[worktype_id].artists.append(artist)
logger.info(f"add artist to response map: {artist} -> {works_map}")
print(f"add artist to response map: {artist} -> {works_map}")
else:
works_map[worktype_id] = ComicWorktypeArtistResponse(
worktype=WorktypeResponse(id=worktype_id, name=work.work_type.name),
artists=[ArtistResponse(id=work.artist.id, name=work.artist.name)]
)
for value in works_map.values():
works.append(value)
response = ComicDetailsResponse(
id=comic.id,
created=str(comic.created_date),
title=str(comic.title),
completed=bool(comic.completed),
current_order=bool(comic.current_order),
weblink=str(comic.weblink),
publisher=comic.publisher.name,
volumes=volumes,
works=works
)
return response
-9
View File
@@ -1,9 +1,6 @@
from typing import List, Dict
from pydantic import BaseModel from pydantic import BaseModel
class ArtistCreation(BaseModel): class ArtistCreation(BaseModel):
id: str id: str
name: str name: str
@@ -11,9 +8,3 @@ class ArtistCreation(BaseModel):
class ArtistResponse(BaseModel): class ArtistResponse(BaseModel):
id: str id: str
name: str name: str
class ArtistDetailResponse(BaseModel):
id: str
name: str
weblink: str
works: Dict[str, List[str]]
@@ -0,0 +1,16 @@
from typing import List
from pydantic import BaseModel
from src.schema.comics.comic import ComicResponse
from src.schema.comics.worktype import WorktypeResponse
class ArtistWorktypeComicResponse(BaseModel):
worktype: WorktypeResponse
comics: List[ComicResponse]
class ArtistDetailResponse(BaseModel):
id: str
name: str
weblink: str
works: List[ArtistWorktypeComicResponse]
+1 -68
View File
@@ -1,12 +1,6 @@
from typing import List, Dict, Optional from typing import Optional
from pydantic import BaseModel, AnyUrl from pydantic import BaseModel, AnyUrl
from src.core.log_conf import logger from src.core.log_conf import logger
from src.db.models.comic import Comic
from src.schema.comics.artist import ArtistResponse
from src.schema.comics.volume import VolumeResponse
from src.schema.comics.worktype import WorktypeResponse
class ComicResponse(BaseModel): class ComicResponse(BaseModel):
@@ -15,70 +9,9 @@ class ComicResponse(BaseModel):
completed: bool completed: bool
class ComicWorktypeArtistResponse(BaseModel):
worktype: WorktypeResponse
artists: List[ArtistResponse]
class ComicDetailsResponse(BaseModel):
id: str
created: str
title: str
completed : bool
current_order : bool
weblink: str
publisher: str
volumes: List[VolumeResponse]
works: List[ComicWorktypeArtistResponse]
class ComicSchema(BaseModel): class ComicSchema(BaseModel):
id: str id: str
title: str title: str
weblink: Optional[AnyUrl] weblink: Optional[AnyUrl]
completed: Optional[bool] completed: Optional[bool]
current_order: Optional[bool] current_order: Optional[bool]
def get_short_info(comic: Comic) -> ComicResponse:
response = ComicResponse(
id=comic.id,
title=str(comic.title),
completed=bool(comic.completed == 1)
)
return response
def get_comic_details(comic: Comic) -> ComicDetailsResponse:
volumes: List[VolumeResponse] = []
for volume in comic.volumes:
volumes.append(VolumeResponse(id=volume.id, name=volume.name))
works: List[ComicWorktypeArtistResponse] = []
works_map: Dict[str, ComicWorktypeArtistResponse] = {}
for work in comic.comic_works:
worktype_id = work.work_type.id
if worktype_id in works_map:
artist = ArtistResponse(id=work.artist.id, name=work.artist.name)
works_map[worktype_id].artists.append(artist)
logger.info(f"add artist to response map: {artist} -> {works_map}")
print(f"add artist to response map: {artist} -> {works_map}")
else:
works_map[worktype_id] = ComicWorktypeArtistResponse(
worktype=WorktypeResponse(id=worktype_id, name=work.work_type.name),
artists=[ArtistResponse(id=work.artist.id, name=work.artist.name)]
)
for value in works_map.values():
works.append(value)
response = ComicDetailsResponse(
id=comic.id,
created=str(comic.created_date),
title=str(comic.title),
completed=bool(comic.completed),
current_order=bool(comic.current_order),
weblink=str(comic.weblink),
publisher=comic.publisher.name,
volumes=volumes,
works=works
)
return response
@@ -0,0 +1,23 @@
from typing import List
from pydantic import BaseModel
from src.schema.comics.artist import ArtistResponse
from src.schema.comics.volume import VolumeResponse
from src.schema.comics.worktype import WorktypeResponse
class ComicWorktypeArtistResponse(BaseModel):
worktype: WorktypeResponse
artists: List[ArtistResponse]
class ComicDetailsResponse(BaseModel):
id: str
created: str
title: str
completed : bool
current_order : bool
weblink: str
publisher: str
volumes: List[VolumeResponse]
works: List[ComicWorktypeArtistResponse]