OPDS Support (#526)

* Added some basic OPDS implementation

* Fixed an issue with feed href

* More changes

* Added library routes and moved user code to a method so we can hack in fixed code without authentication

* Images now load on the OPDS reusing our existing Image infrastructure.

* Added the ability to download and moved some download code to a dedicated service

* Download is working, pagination is implemented.

* Refactored libraries to use pagination

* Laid foundation for OpenSearch implementation

* Fixed up some serialization issues and some old code that wasn't referencing helper methods

* Ensure chapters are sorted when we send them over OPDS

* OpenSearch implemented

* Removed any support for OPDS-PS due to lack of apps supporting it.

* Don't distribute development.json nor stats directory on build.

* Implemented In Progress feed as well.

* Ability to enable OPDS for server. OPDS now accepts initial call as POST in case app uses username/password.

* UI now properly renders state for OPDS enablement. Added Collections routes.

* Fixed pagination startIndex on OPDS feeds when there is less than 1 page.

* Chunky Reader now works. It only accepts UTF-8 encodings

* More Chunky fixes

* More chunky changes, such a fussy client.

* Implemented the ability to have a custom api key assigned to a user and use that api key as your authentication token against OPDS routing.

* Implemented the ability to reset your API Key

* Fixed favicon not being sent back correctly

* Fixed an issue where images wouldn't send on OPDS feed.

* Implemented Page streaming and fixed a pagination bug

* Hooked in the ability to save progress in Kavita when Page Streaming
This commit is contained in:
Joseph Milazzo 2021-08-27 10:19:25 -07:00 committed by GitHub
parent 2a63e5e9e2
commit 6069d93c38
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 2409 additions and 116 deletions

View file

@ -0,0 +1,11 @@
<div class="form-group">
<label for="api-key--{{title}}">{{title}}</label><span *ngIf="tooltipText.length > 0">&nbsp;<i class="fa fa-info-circle" aria-hidden="true" placement="right" [ngbTooltip]="tooltip" role="button" tabindex="0"></i></span>
<ng-template #tooltip>{{tooltipText}}</ng-template>
<div class="input-group">
<input #apiKey type="text" readonly class="form-control" id="api-key--{{title}}" aria-describedby="button-addon4" [value]="key" (click)="selectAll()">
<div class="input-group-append" id="button-addon4">
<button class="btn btn-outline-secondary" type="button" (click)="copy()"><span class="sr-only">Copy</span><i class="fa fa-copy" aria-hidden="true"></i></button>
<button class="btn btn-danger" type="button" (click)="refresh()" *ngIf="showRefresh"><span class="sr-only">Regenerate</span><i class="fa fa-sync-alt" aria-hidden="true"></i></button>
</div>
</div>
</div>

View file

@ -0,0 +1,66 @@
import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ToastrService } from 'ngx-toastr';
import { Subject } from 'rxjs';
import { take, takeUntil } from 'rxjs/operators';
import { ConfirmService } from 'src/app/shared/confirm.service';
import { AccountService } from 'src/app/_services/account.service';
@Component({
selector: 'app-api-key',
templateUrl: './api-key.component.html',
styleUrls: ['./api-key.component.scss']
})
export class ApiKeyComponent implements OnInit, OnDestroy {
@Input() title: string = 'API Key';
@Input() showRefresh: boolean = true;
@Input() transform: (val: string) => string = (val: string) => val;
@Input() tooltipText: string = '';
@ViewChild('apiKey') inputElem!: ElementRef;
key: string = '';
private readonly onDestroy = new Subject<void>();
constructor(private confirmService: ConfirmService, private accountService: AccountService, private toastr: ToastrService) { }
ngOnInit(): void {
this.accountService.currentUser$.pipe(takeUntil(this.onDestroy)).subscribe(user => {
let key = '';
if (user) {
key = user.apiKey;
} else {
key = 'ERROR - KEY NOT SET';
}
if (this.transform != undefined) {
this.key = this.transform(key);
}
});
}
ngOnDestroy() {
this.onDestroy.next();
this.onDestroy.complete();
}
async copy() {
await navigator.clipboard.writeText(this.key);
}
async refresh() {
if (!await this.confirmService.confirm('This will invalidate any OPDS configurations you have setup. Are you sure you want to continue?')) {
return;
}
this.accountService.resetApiKey().subscribe(newKey => {
this.key = newKey;
this.toastr.success('API Key reset');
});
}
selectAll() {
if (this.inputElem) {
this.inputElem.nativeElement.setSelectionRange(0, this.key.length);
}
}
}

View file

@ -190,6 +190,17 @@
</form>
</ng-template>
</ngb-panel>
<ngb-panel id="api-panel" title="OPDS">
<ng-template ngbPanelContent>
<p class="alert alert-danger" role="alert" *ngIf="!opdsEnabled">
OPDS is not enabled on this server!
</p>
<ng-container *ngIf="opdsEnabled">
<app-api-key tooltipText="The API key is like a password. Keep it secret, Keep it safe."></app-api-key>
<app-api-key title="OPDS URL" [showRefresh]="false" [transform]="makeUrl"></app-api-key>
</ng-container>
</ng-template>
</ngb-panel>
</ngb-accordion>
</ng-container>
<ng-container *ngIf="tab.fragment === 'bookmarks'">

View file

@ -10,6 +10,9 @@ import { User } from 'src/app/_models/user';
import { AccountService } from 'src/app/_services/account.service';
import { NavService } from 'src/app/_services/nav.service';
import { ActivatedRoute } from '@angular/router';
import { SettingsService } from 'src/app/admin/settings.service';
import { keyframes } from '@angular/animations';
import { environment } from 'src/environments/environment';
@Component({
selector: 'app-user-preferences',
@ -49,15 +52,16 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
};
fontFamilies: Array<string> = [];
//tabs = ['Preferences', 'Bookmarks'];
tabs: Array<{title: string, fragment: string}> = [
{title: 'Preferences', fragment: ''},
{title: 'Bookmarks', fragment: 'bookmarks'},
];
active = this.tabs[0];
opdsEnabled: boolean = false;
makeUrl: (val: string) => string = (val: string) => {return this.transformKeyToOpdsUrl(val)};
constructor(private accountService: AccountService, private toastr: ToastrService, private bookService: BookService,
private navService: NavService, private titleService: Title, private route: ActivatedRoute) {
private navService: NavService, private titleService: Title, private route: ActivatedRoute, private settingsService: SettingsService) {
this.fontFamilies = this.bookService.getFontFamilies();
this.route.fragment.subscribe(frag => {
@ -68,6 +72,10 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
this.active = this.tabs[0]; // Default to first tab
}
});
this.settingsService.getOpdsEnabled().subscribe(res => {
this.opdsEnabled = res;
})
}
ngOnInit(): void {
@ -177,4 +185,15 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
this.resetPasswordErrors = err;
}));
}
transformKeyToOpdsUrl(key: string) {
let apiUrl = environment.apiUrl;
if (environment.production) {
apiUrl = `${location.protocol}//${location.origin}`;
if (location.port != '80') {
apiUrl += ':' + location.port;
}
}
return `${apiUrl}opds/${key}`;
}
}

View file

@ -6,13 +6,15 @@ import { NgbAccordionModule, NgbNavModule, NgbTooltipModule } from '@ng-bootstra
import { ReactiveFormsModule } from '@angular/forms';
import { NgxSliderModule } from '@angular-slider/ngx-slider';
import { UserSettingsRoutingModule } from './user-settings-routing.module';
import { ApiKeyComponent } from './api-key/api-key.component';
@NgModule({
declarations: [
SeriesBookmarksComponent,
UserPreferencesComponent
UserPreferencesComponent,
ApiKeyComponent
],
imports: [
CommonModule,