diff --git a/src/frontend/src/app/api.service.ts b/src/frontend/src/app/api.service.ts index 110e9c00..81212d69 100644 --- a/src/frontend/src/app/api.service.ts +++ b/src/frontend/src/app/api.service.ts @@ -39,6 +39,7 @@ export class ApiService { API_URL_GENRES_MOVIE = '/api/genres/movie/'; API_URL_GENRES_TV = '/api/genres/tv/'; API_URL_MEDIA_CATEGORIES = '/api/media-categories/'; + API_URL_QUALITIES = '/api/qualities/'; API_URL_QUALITY_PROFILES = '/api/quality-profile/'; API_URL_GIT_COMMIT = '/api/git-commit/'; API_URL_IMPORT_MEDIA_TV = '/api/import/media/tv/'; @@ -55,6 +56,7 @@ export class ApiService { public userToken: string; public users: any; // staff-only list of all users public settings: any; + public qualities: string[] = []; public qualityProfiles: any[] = []; public mediaCategories: string[]; public watchTVSeasons: any[] = []; @@ -180,6 +182,7 @@ export class ApiService { } }) ), + this.fetchQualities(), this.fetchQualityProfiles(), this.fetchMediaCategories(), ]).pipe( @@ -211,7 +214,7 @@ export class ApiService { ); } - public fetchSettings() { + public fetchSettings(): Observable { return this.http.get(this.API_URL_SETTINGS, {headers: this._requestHeaders()}).pipe( map((data: any) => { if (data.length) { @@ -224,7 +227,7 @@ export class ApiService { ); } - public fetchMediaCategories() { + public fetchMediaCategories(): Observable { return this.http.get(this.API_URL_MEDIA_CATEGORIES, {headers: this._requestHeaders()}).pipe( map((data: any) => { if (data.mediaCategories) { @@ -237,7 +240,20 @@ export class ApiService { ); } - public fetchQualityProfiles() { + public fetchQualities(): Observable { + return this.http.get(this.API_URL_QUALITIES, {headers: this._requestHeaders()}).pipe( + map((data: any) => { + if (data.length) { + this.qualities = data; + } else { + console.error('no qualities'); + } + return this.qualities; + }), + ); + } + + public fetchQualityProfiles(): Observable { return this.http.get(this.API_URL_QUALITY_PROFILES, {headers: this._requestHeaders()}).pipe( map((data: any) => { if (data.length) { @@ -303,7 +319,7 @@ export class ApiService { ); } - public login(user: string, pass: string) { + public login(user: string, pass: string): Observable { const params = { username: user, password: pass, @@ -334,7 +350,19 @@ export class ApiService { ); } - public searchTorrents(query: string, mediaType: string) { + public updateQualityProfile(id: number, params: any): Observable { + return this.http.patch(`${this.API_URL_QUALITY_PROFILES}${id}/`, params, {headers: this._requestHeaders()}).pipe( + map((data: any) => { + this.qualityProfiles.forEach((profile, index) => { + if (profile.id === id) { + this.qualityProfiles[index] = params; + } + }) + }), + ); + } + + public searchTorrents(query: string, mediaType: string): Observable { return this.http.get(`${this.API_URL_SEARCH_TORRENTS}?q=${query}&media_type=${mediaType}`, {headers: this._requestHeaders()}).pipe( map((data: any) => { return data; @@ -342,7 +370,7 @@ export class ApiService { ); } - public download(torrentResult: any, mediaType: string, tmdbMedia: any, params?: any) { + public download(torrentResult: any, mediaType: string, tmdbMedia: any, params?: any): Observable { // add extra params Object.assign(params || {}, { torrent: torrentResult, @@ -377,7 +405,7 @@ export class ApiService { ); } - public searchMedia(query: string, mediaType: string, page = 1) { + public searchMedia(query: string, mediaType: string, page = 1): Observable { let params = { q: query, media_type: mediaType, @@ -392,7 +420,7 @@ export class ApiService { ); } - public searchSimilarMedia(tmdbMediaId: string, mediaType: string) { + public searchSimilarMedia(tmdbMediaId: string, mediaType: string): Observable { let params = { tmdb_media_id: tmdbMediaId, media_type: mediaType, @@ -407,7 +435,7 @@ export class ApiService { ); } - public searchRecommendedMedia(tmdbMediaId: string, mediaType: string) { + public searchRecommendedMedia(tmdbMediaId: string, mediaType: string): Observable { let params = { tmdb_media_id: tmdbMediaId, media_type: mediaType, @@ -422,7 +450,7 @@ export class ApiService { ); } - public searchMediaDetail(mediaType: string, id: string) { + public searchMediaDetail(mediaType: string, id: string): Observable { const options = {headers: this._requestHeaders(), params: this._defaultParams()}; return this.http.get(`${this.API_URL_SEARCH_MEDIA}${mediaType}/${id}/`, options).pipe( map((data: any) => { @@ -431,7 +459,7 @@ export class ApiService { ); } - public fetchMediaVideos(mediaType: string, id: string) { + public fetchMediaVideos(mediaType: string, id: string): Observable { const options = {headers: this._requestHeaders()}; return this.http.get(`${this.API_URL_SEARCH_MEDIA}${mediaType}/${id}/videos/`, options).pipe( map((data: any) => { @@ -440,7 +468,7 @@ export class ApiService { ); } - public fetchWatchTVShows(params?: any) { + public fetchWatchTVShows(params?: any): Observable { params = params || {}; const httpParams = new HttpParams({fromObject: params}); return this.http.get(this.API_URL_WATCH_TV_SHOW, {params: httpParams, headers: this._requestHeaders()}).pipe( @@ -451,7 +479,7 @@ export class ApiService { ); } - public fetchWatchTVSeasons(params?: any) { + public fetchWatchTVSeasons(params?: any): Observable { params = params || {}; const httpParams = new HttpParams({fromObject: params}); return this.http.get(this.API_URL_WATCH_TV_SEASON, {params: httpParams, headers: this._requestHeaders()}).pipe( @@ -467,7 +495,7 @@ export class ApiService { ); } - public fetchWatchTVSeasonRequests(params?: any) { + public fetchWatchTVSeasonRequests(params?: any): Observable { params = params || {}; const httpParams = new HttpParams({fromObject: params}); return this.http.get(this.API_URL_WATCH_TV_SEASON_REQUEST, {params: httpParams, headers: this._requestHeaders()}).pipe( @@ -483,7 +511,7 @@ export class ApiService { ); } - public fetchWatchMovies(params?: any) { + public fetchWatchMovies(params?: any): Observable { params = params || {}; const httpParams = new HttpParams({fromObject: params}); @@ -500,7 +528,7 @@ export class ApiService { ); } - public fetchWatchTVEpisodes(params: any) { + public fetchWatchTVEpisodes(params: any): Observable { const httpParams = new HttpParams({fromObject: params}); return this.http.get(this.API_URL_WATCH_TV_EPISODE, {headers: this._requestHeaders(), params: httpParams}).pipe( map((records: any) => { @@ -515,7 +543,7 @@ export class ApiService { ); } - public fetchCurrentTorrents(params: any) { + public fetchCurrentTorrents(params: any): Observable { const httpParams = new HttpParams({fromObject: params}); return this.http.get(this.API_URL_CURRENT_TORRENTS, {headers: this._requestHeaders(), params: httpParams}).pipe( map((data: any) => { @@ -734,7 +762,7 @@ export class ApiService { ); } - public verifySettings() { + public verifySettings(): Observable { return this.http.get(`${this.API_URL_SETTINGS}${this.settings.id}/verify/`, {headers: this._requestHeaders()}).pipe( map((data: any) => { return data; @@ -801,15 +829,15 @@ export class ApiService { return this._discoverMedia(this.SEARCH_MEDIA_TYPE_TV, params); } - public fetchMovieGenres() { + public fetchMovieGenres(): Observable { return this._fetchGenres(this.SEARCH_MEDIA_TYPE_MOVIE); } - public fetchTVGenres() { + public fetchTVGenres(): Observable { return this._fetchGenres(this.SEARCH_MEDIA_TYPE_TV); } - public verifyJackettIndexers() { + public verifyJackettIndexers(): Observable { return this.http.get(`${this.API_URL_SETTINGS}${this.settings.id}/verify-jackett-indexers/`, {headers: this._requestHeaders()}); } @@ -837,7 +865,7 @@ export class ApiService { return this.http.get(url, {params: httpParams, headers: this._requestHeaders()}); } - public openSubtitlesAuth() { + public openSubtitlesAuth(): Observable { const url = this.API_URL_OPEN_SUBTITLES_AUTH; return this.http.post(url, null, {headers: this._requestHeaders()}); } @@ -961,13 +989,13 @@ export class ApiService { this._updateStorage().subscribe(); } - protected _fetchGenres(mediaType: string) { + protected _fetchGenres(mediaType: string): Observable { const url = mediaType === this.SEARCH_MEDIA_TYPE_MOVIE ? this.API_URL_GENRES_MOVIE : this.API_URL_GENRES_TV; const params = this._defaultParams(); return this.http.get(url, {headers: this._requestHeaders(), params: params}); } - protected _discoverMedia(mediaType: string, params: any) { + protected _discoverMedia(mediaType: string, params: any): Observable { params = Object.assign(params, this._defaultParams()); const httpParams = new HttpParams({fromObject: params}); const url = mediaType === this.SEARCH_MEDIA_TYPE_MOVIE ? this.API_URL_DISCOVER_MOVIES : this.API_URL_DISCOVER_TV; diff --git a/src/frontend/src/app/settings/quality-profiles.component.html b/src/frontend/src/app/settings/quality-profiles.component.html index 7629b553..1d63e72b 100644 --- a/src/frontend/src/app/settings/quality-profiles.component.html +++ b/src/frontend/src/app/settings/quality-profiles.component.html @@ -3,6 +3,7 @@ - diff --git a/src/frontend/src/app/settings/quality-profiles.component.ts b/src/frontend/src/app/settings/quality-profiles.component.ts index 75174f37..4ef40e99 100644 --- a/src/frontend/src/app/settings/quality-profiles.component.ts +++ b/src/frontend/src/app/settings/quality-profiles.component.ts @@ -1,11 +1,10 @@ import {Component, OnInit} from '@angular/core'; import {NgbActiveModal} from "@ng-bootstrap/ng-bootstrap"; import {ApiService} from "../api.service"; -import {FormArray, FormBuilder, FormGroup, Validators} from "@angular/forms"; +import {FormArray, FormBuilder, FormControl, FormGroup, Validators} from "@angular/forms"; import {ToastrService} from 'ngx-toastr'; // TODO - add/remove profiles -// TODO - implement UI (html & angular) validation @Component({ selector: 'app-quality-profiles', @@ -13,8 +12,8 @@ import {ToastrService} from 'ngx-toastr'; styleUrl: './quality-profiles.component.css' }) export class QualityProfilesComponent implements OnInit { - - public form: FormGroup<{ profiles: FormArray }>; + public isLoading = false; + public form: FormGroup<{ profiles: FormArray }>; constructor( public apiService: ApiService, @@ -29,24 +28,28 @@ export class QualityProfilesComponent implements OnInit { profiles: this.fb.array(this.apiService.qualityProfiles.map(p => this.fb.group({ id: p.id, name: this.fb.control(p.name, [Validators.required, Validators.minLength(2)]), - profile: this.fb.control(p.profile, [Validators.required]), + quality: this.fb.control(p.quality, [Validators.required]), min_size_gb: this.fb.control(p.min_size_gb, [Validators.min(0)]), max_size_gb: this.fb.control(p.max_size_gb, [Validators.min(0)]), require_hdr: p.require_hdr, require_five_point_one: p.require_five_point_one, }))), }); + } - this.form.valueChanges.subscribe({ - next: (data) => { - console.log(data); + public save(profileFormGroup: FormGroup) { + this.isLoading = true; + const data = profileFormGroup.value; + this.apiService.updateQualityProfile(data.id, data).subscribe({ + next: () => { + this.toastr.success('Successfully updated quality profile'); + this.isLoading = false; + }, + error: (error) => { + console.error(error); + this.toastr.error('An unknown error occurred updating the quality profile'); + this.isLoading = false; } }) } - - public save() { - // TODO - implement save - this.toastr.error('SAVE TODO') - this.activeModal.close() - } } diff --git a/src/nefarious/api/urls.py b/src/nefarious/api/urls.py index d8f3fd27..b86c5e75 100644 --- a/src/nefarious/api/urls.py +++ b/src/nefarious/api/urls.py @@ -34,6 +34,7 @@ path('import/media//', views.ImportMediaLibraryView.as_view()), path('genres//', views.GenresView.as_view()), path('media-categories/', views.MediaCategoriesView.as_view()), + path('qualities/', views.QualitiesView.as_view()), path('auth/', views.ObtainAuthTokenView.as_view()), # authenticates user and returns token path('git-commit/', views.GitCommitView.as_view()), # returns this app's git commit path('open-subtitles/auth/', views.OpenSubtitlesAuthView.as_view()), # auths against open subtitles diff --git a/src/nefarious/api/views.py b/src/nefarious/api/views.py index 961cb6a5..699ac0c2 100644 --- a/src/nefarious/api/views.py +++ b/src/nefarious/api/views.py @@ -570,3 +570,10 @@ def post(self, request): assert 'message' in request.data, 'missing notification message' return Response({'success': send_message(request.data['message'])}) + +@method_decorator(gzip_page, name='dispatch') +class QualitiesView(views.APIView): + + def get(self, request): + return Response([p.name for p in PROFILES]) + diff --git a/src/nefarious/migrations/0090_auto_20240812_2209.py b/src/nefarious/migrations/0090_auto_20240812_2209.py new file mode 100644 index 00000000..a64475c0 --- /dev/null +++ b/src/nefarious/migrations/0090_auto_20240812_2209.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.2 on 2024-08-12 22:09 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('nefarious', '0089_qualityprofile_require_five_point_one'), + ] + + operations = [ + migrations.RenameField( + model_name='qualityprofile', + old_name='profile', + new_name='quality', + ), + ] diff --git a/src/nefarious/models.py b/src/nefarious/models.py index 0713c5c9..ef5ca97d 100644 --- a/src/nefarious/models.py +++ b/src/nefarious/models.py @@ -19,8 +19,10 @@ class QualityProfile(models.Model): + # TODO - when "stop watching" the quality profile select box goes empty + # TODO - after deleting a quality profile, swap out default movie/tv profiles to something else...never let user remove all profiles? name = models.CharField(max_length=500, unique=True) - profile = models.CharField(max_length=500, choices=zip(quality.PROFILE_NAMES, quality.PROFILE_NAMES)) + quality = models.CharField(max_length=500, choices=zip(quality.PROFILE_NAMES, quality.PROFILE_NAMES)) min_size_gb = models.DecimalField( null=True, blank=True, max_digits=10, decimal_places=2, validators=[MinValueValidator(0)], help_text='minimum size (gb) to download') max_size_gb = models.DecimalField( @@ -29,9 +31,9 @@ class QualityProfile(models.Model): require_five_point_one = models.BooleanField(default=False, help_text='media must be in 5.1 surround sound (e.g. Dolby 5.1)') def __str__(self): - if self.name == self.profile: + if self.name == self.quality: return self.name - return f'{self.name} ({self.profile})' + return f'{self.name} ({self.quality})' class NefariousSettings(models.Model): diff --git a/src/nefarious/processors.py b/src/nefarious/processors.py index f0e81a35..9ba63654 100644 --- a/src/nefarious/processors.py +++ b/src/nefarious/processors.py @@ -116,7 +116,7 @@ def fetch(self): def is_match(self, title: str, size_kb: int) -> bool: parser = self._get_parser(title) quality_profile = self._get_quality_profile() - profile = Profile.get_from_name(quality_profile.profile) + profile = Profile.get_from_name(quality_profile.quality) size_gb = size_kb / (1024**2) mismatch = None