Merge branch 'fullydeleable' into 'master'

Fully Deletable Videos

Closes #74

See merge request lukas/openmediacenter!52
This commit is contained in:
Lukas Heiligenbrunner 2021-09-05 13:14:48 +00:00
commit 2a098527bd
13 changed files with 260 additions and 36 deletions

View File

@ -15,7 +15,8 @@ func readVideosFromResultset(rows *sql.Rows) []types.VideoUnloadedType {
var vid types.VideoUnloadedType var vid types.VideoUnloadedType
err := rows.Scan(&vid.MovieId, &vid.MovieName) err := rows.Scan(&vid.MovieId, &vid.MovieName)
if err != nil { if err != nil {
panic(err.Error()) // proper error handling instead of panic in your app fmt.Println(err.Error())
return nil
} }
result = append(result, vid) result = append(result, vid)
} }

View File

@ -73,6 +73,7 @@ func getSettingsFromDB() {
VideoPath string VideoPath string
TVShowPath string TVShowPath string
TVShowEnabled bool TVShowEnabled bool
FullDeleteEnabled bool
} }
regexMatchUrl := regexp.MustCompile("^http(|s)://([0-9]){1,3}\\.([0-9]){1,3}\\.([0-9]){1,3}\\.([0-9]){1,3}:[0-9]{1,5}") regexMatchUrl := regexp.MustCompile("^http(|s)://([0-9]){1,3}\\.([0-9]){1,3}\\.([0-9]){1,3}\\.([0-9]){1,3}:[0-9]{1,5}")
@ -88,6 +89,7 @@ func getSettingsFromDB() {
VideoPath: serverVideoPath, VideoPath: serverVideoPath,
TVShowPath: serverTVShowPath, TVShowPath: serverTVShowPath,
TVShowEnabled: settings.TVShowsEnabled(), TVShowEnabled: settings.TVShowsEnabled(),
FullDeleteEnabled: settings.VideosDeletable(),
} }
str, _ := json.Marshal(res) str, _ := json.Marshal(res)

View File

@ -6,6 +6,8 @@ import (
"net/url" "net/url"
"openmediacenter/apiGo/api/types" "openmediacenter/apiGo/api/types"
"openmediacenter/apiGo/database" "openmediacenter/apiGo/database"
"openmediacenter/apiGo/database/settings"
"os"
"strconv" "strconv"
) )
@ -418,12 +420,14 @@ func addToVideoHandlers() {
* @apiGroup video * @apiGroup video
* *
* @apiParam {int} MovieId ID of video * @apiParam {int} MovieId ID of video
* @apiParam {bool} FullyDelete Delete video from disk?
* *
* @apiSuccess {string} result 'success' if successfully or error message if not * @apiSuccess {string} result 'success' if successfully or error message if not
*/ */
AddHandler("deleteVideo", VideoNode, func(info *HandlerInfo) []byte { AddHandler("deleteVideo", VideoNode, func(info *HandlerInfo) []byte {
var args struct { var args struct {
MovieId int MovieId int
FullyDelete bool
} }
if err := FillStruct(&args, info.Data); err != nil { if err := FillStruct(&args, info.Data); err != nil {
fmt.Println(err.Error()) fmt.Println(err.Error())
@ -443,6 +447,26 @@ func addToVideoHandlers() {
return database.ManualSuccessResponse(err) return database.ManualSuccessResponse(err)
} }
// only allow deletion of video if cli flag is set, independent of passed api arg
if settings.VideosDeletable() && args.FullyDelete {
// get physical path of video to delete
query = fmt.Sprintf("SELECT movie_url FROM videos WHERE movie_id=%d", args.MovieId)
var vidpath string
err := database.QueryRow(query).Scan(&vidpath)
if err != nil {
return database.ManualSuccessResponse(err)
}
assembledPath := database.SettingsVideoPrefix + "/" + vidpath
err = os.Remove(assembledPath)
if err != nil {
fmt.Printf("unable to delete file: %s -- %s\n", assembledPath, err.Error())
return database.ManualSuccessResponse(err)
}
}
// delete video row from db
query = fmt.Sprintf("DELETE FROM videos WHERE movie_id=%d", args.MovieId) query = fmt.Sprintf("DELETE FROM videos WHERE movie_id=%d", args.MovieId)
return database.SuccessQuery(query) return database.SuccessQuery(query)
}) })

View File

@ -1,6 +1,7 @@
package settings package settings
var tvShowEnabled bool var tvShowEnabled bool
var videosDeletable bool
func TVShowsEnabled() bool { func TVShowsEnabled() bool {
return tvShowEnabled return tvShowEnabled
@ -9,3 +10,11 @@ func TVShowsEnabled() bool {
func SetTVShowEnabled(enabled bool) { func SetTVShowEnabled(enabled bool) {
tvShowEnabled = enabled tvShowEnabled = enabled
} }
func VideosDeletable() bool {
return videosDeletable
}
func SetVideosDeletable(deletable bool) {
videosDeletable = deletable
}

View File

@ -57,10 +57,12 @@ func handleCommandLineArguments() (*database.DatabaseConfig, bool, *string) {
pathPrefix := flag.String("ReindexPrefix", "/var/www/openmediacenter", "Prefix path for videos to reindex") pathPrefix := flag.String("ReindexPrefix", "/var/www/openmediacenter", "Prefix path for videos to reindex")
disableTVShowSupport := flag.Bool("DisableTVSupport", false, "Disable the TVShow support and pages") disableTVShowSupport := flag.Bool("DisableTVSupport", false, "Disable the TVShow support and pages")
videosFullyDeletable := flag.Bool("FullyDeletableVideos", false, "Allow deletion from harddisk")
flag.Parse() flag.Parse()
settings2.SetTVShowEnabled(!*disableTVShowSupport) settings2.SetTVShowEnabled(!*disableTVShowSupport)
settings2.SetVideosDeletable(*videosFullyDeletable)
return &database.DatabaseConfig{ return &database.DatabaseConfig{
DBHost: *dbhostPtr, DBHost: *dbhostPtr,

View File

@ -26,7 +26,7 @@
"build": "CI=false react-scripts build", "build": "CI=false react-scripts build",
"test": "CI=true react-scripts test --reporters=jest-junit --verbose --silent --coverage --reporters=default", "test": "CI=true react-scripts test --reporters=jest-junit --verbose --silent --coverage --reporters=default",
"lint": "eslint --format gitlab src/", "lint": "eslint --format gitlab src/",
"apidoc": "apidoc -i apiGo/ -o doc/" "apidoc": "apidoc --single -i apiGo/ -o doc/"
}, },
"jest": { "jest": {
"collectCoverageFrom": [ "collectCoverageFrom": [

View File

@ -87,6 +87,7 @@ class App extends React.Component<{}, state> {
GlobalInfos.setVideoPaths(result.VideoPath, result.TVShowPath); GlobalInfos.setVideoPaths(result.VideoPath, result.TVShowPath);
GlobalInfos.setTVShowsEnabled(result.TVShowEnabled); GlobalInfos.setTVShowsEnabled(result.TVShowEnabled);
GlobalInfos.setFullDeleteEnabled(result.FullDeleteEnabled);
this.setState({ this.setState({
mediacentername: result.MediacenterName mediacentername: result.MediacenterName

View File

@ -0,0 +1,51 @@
import {shallow} from 'enzyme';
import React from 'react';
import {ButtonPopup} from './ButtonPopup';
import exp from "constants";
describe('<ButtonPopup/>', function () {
it('renders without crashing ', function () {
const wrapper = shallow(<ButtonPopup/>);
wrapper.unmount();
});
it('renders two buttons', function () {
const wrapper = shallow(<ButtonPopup/>);
expect(wrapper.find('Button')).toHaveLength(2);
});
it('renders three buttons if alternative defined', function () {
const wrapper = shallow(<ButtonPopup AlternativeButtonTitle='alt'/>);
expect(wrapper.find('Button')).toHaveLength(3);
});
it('test click handlings', function () {
const althandler = jest.fn();
const denyhandler = jest.fn();
const submithandler = jest.fn();
const wrapper = shallow(<ButtonPopup DenyButtonTitle='deny' onDeny={denyhandler} SubmitButtonTitle='submit'
onSubmit={submithandler} AlternativeButtonTitle='alt'
onAlternativeButton={althandler}/>);
wrapper.find('Button').findWhere(e => e.props().title === "deny").simulate('click');
expect(denyhandler).toHaveBeenCalledTimes(1);
wrapper.find('Button').findWhere(e => e.props().title === "alt").simulate('click');
expect(althandler).toHaveBeenCalledTimes(1);
wrapper.find('Button').findWhere(e => e.props().title === "submit").simulate('click');
expect(submithandler).toHaveBeenCalledTimes(1);
});
it('test Parentsubmit and parenthide callbacks', function () {
const ondeny = jest.fn();
const onsubmit = jest.fn();
const wrapper = shallow(<ButtonPopup DenyButtonTitle='deny' SubmitButtonTitle='submit' onDeny={ondeny} onSubmit={onsubmit} AlternativeButtonTitle='alt'/>);
wrapper.find('PopupBase').props().onHide();
expect(ondeny).toHaveBeenCalledTimes(1);
wrapper.find('PopupBase').props().ParentSubmit();
expect(onsubmit).toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,58 @@
import React from 'react';
import PopupBase from '../PopupBase';
import {Button} from '../../GPElements/Button';
/**
* Delete Video popup
* can only be rendered once!
* @constructor
*/
export const ButtonPopup = (props: {
onSubmit: () => void;
onDeny: () => void;
onAlternativeButton?: () => void;
SubmitButtonTitle: string;
DenyButtonTitle: string;
AlternativeButtonTitle?: string;
Title: string;
}): JSX.Element => {
return (
<>
<PopupBase
title={props.Title}
onHide={(): void => props.onDeny()}
height='200px'
width='400px'
ParentSubmit={(): void => {
props.onSubmit();
}}>
<Button
onClick={(): void => {
props.onSubmit();
}}
title={props.SubmitButtonTitle}
/>
{props.AlternativeButtonTitle ? (
<Button
color={{backgroundColor: 'darkorange'}}
onClick={(): void => {
props.onAlternativeButton ? props.onAlternativeButton() : null;
}}
title={props.AlternativeButtonTitle}
/>
) : (
<></>
)}
<Button
color={{backgroundColor: 'red'}}
onClick={(): void => {
props.onDeny();
}}
title={props.DenyButtonTitle}
/>
</PopupBase>
</>
);
};

View File

@ -2,6 +2,7 @@ import {shallow} from 'enzyme';
import React from 'react'; import React from 'react';
import {Player} from './Player'; import {Player} from './Player';
import {callAPI} from '../../utils/Api'; import {callAPI} from '../../utils/Api';
import GlobalInfos from "../../utils/GlobalInfos";
describe('<Player/>', function () { describe('<Player/>', function () {
@ -84,23 +85,54 @@ describe('<Player/>', function () {
expect(wrapper.find('AddTagPopup')).toHaveLength(1); expect(wrapper.find('AddTagPopup')).toHaveLength(1);
}); });
it('test delete button', done => { it('test fully delete popup rendering', function () {
const wrapper = instance(); const wrapper = instance();
// allow videos to be fully deletable
GlobalInfos.setFullDeleteEnabled(true);
wrapper.setProps({history: {goBack: jest.fn()}}); wrapper.setState({deletepopupvisible: true});
global.fetch = prepareFetchApi({result: 'success'}); expect(wrapper.find('ButtonPopup')).toHaveLength(1)
});
it('test delete popup rendering', function () {
const wrapper = instance();
GlobalInfos.setFullDeleteEnabled(false);
wrapper.setState({deletepopupvisible: true});
expect(wrapper.find('ButtonPopup')).toHaveLength(1)
});
it('test delete button', () => {
const wrapper = instance();
const callback = jest.fn();
wrapper.setProps({history: {goBack: callback}});
callAPIMock({result: 'success'})
GlobalInfos.setFullDeleteEnabled(false);
// request the popup to pop
wrapper.find('.videoactions').find('Button').at(2).simulate('click'); wrapper.find('.videoactions').find('Button').at(2).simulate('click');
process.nextTick(() => { // click the first submit button
// refetch is called so fetch called 3 times wrapper.find('ButtonPopup').dive().find('Button').at(0).simulate('click')
expect(global.fetch).toHaveBeenCalledTimes(1);
expect(wrapper.instance().props.history.goBack).toHaveBeenCalledTimes(1);
global.fetch.mockClear(); // refetch is called so fetch called 3 times
done(); expect(callAPI).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledTimes(1);
// now lets test if this works also with the fullydeletepopup
GlobalInfos.setFullDeleteEnabled(true);
// request the popup to pop
wrapper.setState({deletepopupvisible: true}, () => {
// click the first submit button
wrapper.find('ButtonPopup').dive().find('Button').at(0).simulate('click')
expect(callAPI).toHaveBeenCalledTimes(2);
expect(callback).toHaveBeenCalledTimes(2);
}); });
}); });
@ -152,16 +184,14 @@ describe('<Player/>', function () {
it('test click of quickadd tag btn', done => { it('test click of quickadd tag btn', done => {
const wrapper = generatetag(); const wrapper = generatetag();
global.fetch = prepareFetchApi({result: 'success'}); callAPIMock({result: 'success'})
// render tag subcomponent // render tag subcomponent
const tag = wrapper.find('Tag').first().dive(); const tag = wrapper.find('Tag').first().dive();
tag.simulate('click'); tag.simulate('click');
process.nextTick(() => { process.nextTick(() => {
expect(global.fetch).toHaveBeenCalledTimes(1); expect(callAPI).toHaveBeenCalledTimes(1);
global.fetch.mockClear();
done(); done();
}); });
}); });
@ -169,7 +199,7 @@ describe('<Player/>', function () {
it('test failing quickadd', done => { it('test failing quickadd', done => {
const wrapper = generatetag(); const wrapper = generatetag();
global.fetch = prepareFetchApi({result: 'nonsuccess'}); callAPIMock({result: 'nonsuccess'});
global.console.error = jest.fn(); global.console.error = jest.fn();
// render tag subcomponent // render tag subcomponent
@ -178,8 +208,6 @@ describe('<Player/>', function () {
process.nextTick(() => { process.nextTick(() => {
expect(global.console.error).toHaveBeenCalledTimes(2); expect(global.console.error).toHaveBeenCalledTimes(2);
global.fetch.mockClear();
done(); done();
}); });
}); });

View File

@ -21,6 +21,7 @@ import PlyrJS from 'plyr';
import {Button} from '../../elements/GPElements/Button'; import {Button} from '../../elements/GPElements/Button';
import {VideoTypes} from '../../types/ApiTypes'; import {VideoTypes} from '../../types/ApiTypes';
import GlobalInfos from '../../utils/GlobalInfos'; import GlobalInfos from '../../utils/GlobalInfos';
import {ButtonPopup} from '../../elements/Popups/ButtonPopup/ButtonPopup';
interface Props extends RouteComponentProps<{id: string}> {} interface Props extends RouteComponentProps<{id: string}> {}
@ -35,6 +36,7 @@ interface mystate {
suggesttag: TagType[]; suggesttag: TagType[];
popupvisible: boolean; popupvisible: boolean;
actorpopupvisible: boolean; actorpopupvisible: boolean;
deletepopupvisible: boolean;
actors: ActorType[]; actors: ActorType[];
} }
@ -56,6 +58,7 @@ export class Player extends React.Component<Props, mystate> {
suggesttag: [], suggesttag: [],
popupvisible: false, popupvisible: false,
actorpopupvisible: false, actorpopupvisible: false,
deletepopupvisible: false,
actors: [] actors: []
}; };
@ -91,7 +94,7 @@ export class Player extends React.Component<Props, mystate> {
<Button <Button
title='Delete Video' title='Delete Video'
onClick={(): void => { onClick={(): void => {
this.deleteVideo(); this.setState({deletepopupvisible: true});
}} }}
color={{backgroundColor: 'red'}} color={{backgroundColor: 'red'}}
/> />
@ -196,10 +199,46 @@ export class Player extends React.Component<Props, mystate> {
movieId={this.state.movieId} movieId={this.state.movieId}
/> />
) : null} ) : null}
{this.state.deletepopupvisible ? this.renderDeletePopup() : null}
</> </>
); );
} }
renderDeletePopup(): JSX.Element {
if (GlobalInfos.isVideoFulldeleteable()) {
return (
<ButtonPopup
onDeny={(): void => this.setState({deletepopupvisible: false})}
onSubmit={(): void => {
this.setState({deletepopupvisible: false});
this.deleteVideo(true);
}}
onAlternativeButton={(): void => {
this.setState({deletepopupvisible: false});
this.deleteVideo(false);
}}
DenyButtonTitle='Cancel'
SubmitButtonTitle='Fully Delete!'
Title='Fully Delete Video?'
AlternativeButtonTitle='Reference Only'
/>
);
} else {
return (
<ButtonPopup
onDeny={(): void => this.setState({deletepopupvisible: false})}
onSubmit={(): void => {
this.setState({deletepopupvisible: false});
this.deleteVideo(false);
}}
DenyButtonTitle='Cancel'
SubmitButtonTitle='Delete Video Reference!'
Title='Delete Video?'
/>
);
}
}
/** /**
* quick add callback to add tag to db and change gui correctly * quick add callback to add tag to db and change gui correctly
* @param tagId id of tag to add * @param tagId id of tag to add
@ -325,17 +364,16 @@ export class Player extends React.Component<Props, mystate> {
/** /**
* delete the current video and return to last page * delete the current video and return to last page
*/ */
deleteVideo(): void { deleteVideo(fullyDelete: boolean): void {
callAPI( callAPI(
APINode.Video, APINode.Video,
{action: 'deleteVideo', MovieId: parseInt(this.props.match.params.id, 10)}, {action: 'deleteVideo', MovieId: parseInt(this.props.match.params.id, 10), FullyDelete: fullyDelete},
(result: GeneralSuccess) => { (result: GeneralSuccess) => {
if (result.result === 'success') { if (result.result === 'success') {
// return to last element if successful // return to last element if successful
this.props.history.goBack(); this.props.history.goBack();
} else { } else {
console.error('an error occured while liking'); console.error('an error occured while deleting the video: ' + JSON.stringify(result));
console.error(result);
} }
} }
); );

View File

@ -37,6 +37,7 @@ export namespace SettingsTypes {
VideoPath: string; VideoPath: string;
TVShowPath: string; TVShowPath: string;
TVShowEnabled: boolean; TVShowEnabled: boolean;
FullDeleteEnabled: boolean;
} }
export interface SettingsType { export interface SettingsType {

View File

@ -10,6 +10,7 @@ class StaticInfos {
private videopath: string = ''; private videopath: string = '';
private tvshowpath: string = ''; private tvshowpath: string = '';
private TVShowsEnabled: boolean = false; private TVShowsEnabled: boolean = false;
private fullDeleteable: boolean = false;
/** /**
* check if the current theme is the dark theme * check if the current theme is the dark theme
@ -80,6 +81,14 @@ class StaticInfos {
isTVShowEnabled(): boolean { isTVShowEnabled(): boolean {
return this.TVShowsEnabled; return this.TVShowsEnabled;
} }
setFullDeleteEnabled(FullDeleteEnabled: boolean): void {
this.fullDeleteable = FullDeleteEnabled;
}
isVideoFulldeleteable(): boolean {
return this.fullDeleteable;
}
} }
export default new StaticInfos(); export default new StaticInfos();