diff --git a/apiGo/api/Helpers.go b/apiGo/api/Helpers.go
index aac7b93..1696c57 100644
--- a/apiGo/api/Helpers.go
+++ b/apiGo/api/Helpers.go
@@ -15,7 +15,8 @@ func readVideosFromResultset(rows *sql.Rows) []types.VideoUnloadedType {
var vid types.VideoUnloadedType
err := rows.Scan(&vid.MovieId, &vid.MovieName)
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)
}
diff --git a/apiGo/api/Settings.go b/apiGo/api/Settings.go
index 711d4de..b3a6a63 100644
--- a/apiGo/api/Settings.go
+++ b/apiGo/api/Settings.go
@@ -67,12 +67,13 @@ func getSettingsFromDB() {
sett := settings.LoadSettings()
type InitialDataTypeResponse struct {
- DarkMode bool
- Pasword bool
- MediacenterName string
- VideoPath string
- TVShowPath string
- TVShowEnabled bool
+ DarkMode bool
+ Pasword bool
+ MediacenterName string
+ VideoPath string
+ TVShowPath string
+ 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}")
@@ -82,12 +83,13 @@ func getSettingsFromDB() {
serverTVShowPath := strings.TrimPrefix(sett.TVShowPath, tvshowurl)
res := InitialDataTypeResponse{
- DarkMode: sett.DarkMode,
- Pasword: sett.Pasword != "-1",
- MediacenterName: sett.MediacenterName,
- VideoPath: serverVideoPath,
- TVShowPath: serverTVShowPath,
- TVShowEnabled: settings.TVShowsEnabled(),
+ DarkMode: sett.DarkMode,
+ Pasword: sett.Pasword != "-1",
+ MediacenterName: sett.MediacenterName,
+ VideoPath: serverVideoPath,
+ TVShowPath: serverTVShowPath,
+ TVShowEnabled: settings.TVShowsEnabled(),
+ FullDeleteEnabled: settings.VideosDeletable(),
}
str, _ := json.Marshal(res)
diff --git a/apiGo/api/Video.go b/apiGo/api/Video.go
index 9e6c1d2..373d3a2 100644
--- a/apiGo/api/Video.go
+++ b/apiGo/api/Video.go
@@ -6,6 +6,8 @@ import (
"net/url"
"openmediacenter/apiGo/api/types"
"openmediacenter/apiGo/database"
+ "openmediacenter/apiGo/database/settings"
+ "os"
"strconv"
)
@@ -418,12 +420,14 @@ func addToVideoHandlers() {
* @apiGroup 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
*/
AddHandler("deleteVideo", VideoNode, func(info *HandlerInfo) []byte {
var args struct {
- MovieId int
+ MovieId int
+ FullyDelete bool
}
if err := FillStruct(&args, info.Data); err != nil {
fmt.Println(err.Error())
@@ -443,6 +447,26 @@ func addToVideoHandlers() {
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)
return database.SuccessQuery(query)
})
diff --git a/apiGo/database/settings/Settings.go b/apiGo/database/settings/Settings.go
index 409a071..d13d305 100644
--- a/apiGo/database/settings/Settings.go
+++ b/apiGo/database/settings/Settings.go
@@ -1,6 +1,7 @@
package settings
var tvShowEnabled bool
+var videosDeletable bool
func TVShowsEnabled() bool {
return tvShowEnabled
@@ -9,3 +10,11 @@ func TVShowsEnabled() bool {
func SetTVShowEnabled(enabled bool) {
tvShowEnabled = enabled
}
+
+func VideosDeletable() bool {
+ return videosDeletable
+}
+
+func SetVideosDeletable(deletable bool) {
+ videosDeletable = deletable
+}
diff --git a/apiGo/main.go b/apiGo/main.go
index 80e138c..9d96e0e 100644
--- a/apiGo/main.go
+++ b/apiGo/main.go
@@ -57,10 +57,12 @@ func handleCommandLineArguments() (*database.DatabaseConfig, bool, *string) {
pathPrefix := flag.String("ReindexPrefix", "/var/www/openmediacenter", "Prefix path for videos to reindex")
disableTVShowSupport := flag.Bool("DisableTVSupport", false, "Disable the TVShow support and pages")
+ videosFullyDeletable := flag.Bool("FullyDeletableVideos", false, "Allow deletion from harddisk")
flag.Parse()
settings2.SetTVShowEnabled(!*disableTVShowSupport)
+ settings2.SetVideosDeletable(*videosFullyDeletable)
return &database.DatabaseConfig{
DBHost: *dbhostPtr,
diff --git a/package.json b/package.json
index 6bc2499..1890f28 100644
--- a/package.json
+++ b/package.json
@@ -26,7 +26,7 @@
"build": "CI=false react-scripts build",
"test": "CI=true react-scripts test --reporters=jest-junit --verbose --silent --coverage --reporters=default",
"lint": "eslint --format gitlab src/",
- "apidoc": "apidoc -i apiGo/ -o doc/"
+ "apidoc": "apidoc --single -i apiGo/ -o doc/"
},
"jest": {
"collectCoverageFrom": [
diff --git a/src/App.tsx b/src/App.tsx
index db339d0..d78d444 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -87,6 +87,7 @@ class App extends React.Component<{}, state> {
GlobalInfos.setVideoPaths(result.VideoPath, result.TVShowPath);
GlobalInfos.setTVShowsEnabled(result.TVShowEnabled);
+ GlobalInfos.setFullDeleteEnabled(result.FullDeleteEnabled);
this.setState({
mediacentername: result.MediacenterName
diff --git a/src/elements/Popups/ButtonPopup/ButtonPopup.test.js b/src/elements/Popups/ButtonPopup/ButtonPopup.test.js
new file mode 100644
index 0000000..9ce12ea
--- /dev/null
+++ b/src/elements/Popups/ButtonPopup/ButtonPopup.test.js
@@ -0,0 +1,51 @@
+import {shallow} from 'enzyme';
+import React from 'react';
+import {ButtonPopup} from './ButtonPopup';
+import exp from "constants";
+
+describe('', function () {
+ it('renders without crashing ', function () {
+ const wrapper = shallow();
+ wrapper.unmount();
+ });
+
+ it('renders two buttons', function () {
+ const wrapper = shallow();
+ expect(wrapper.find('Button')).toHaveLength(2);
+ });
+
+ it('renders three buttons if alternative defined', function () {
+ const wrapper = shallow();
+ 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();
+ 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();
+ wrapper.find('PopupBase').props().onHide();
+ expect(ondeny).toHaveBeenCalledTimes(1);
+
+ wrapper.find('PopupBase').props().ParentSubmit();
+ expect(onsubmit).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/src/elements/Popups/ButtonPopup/ButtonPopup.tsx b/src/elements/Popups/ButtonPopup/ButtonPopup.tsx
new file mode 100644
index 0000000..3b5b667
--- /dev/null
+++ b/src/elements/Popups/ButtonPopup/ButtonPopup.tsx
@@ -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 (
+ <>
+ props.onDeny()}
+ height='200px'
+ width='400px'
+ ParentSubmit={(): void => {
+ props.onSubmit();
+ }}>
+
+ >
+ );
+};
diff --git a/src/pages/Player/Player.test.js b/src/pages/Player/Player.test.js
index aed56cd..92206a8 100644
--- a/src/pages/Player/Player.test.js
+++ b/src/pages/Player/Player.test.js
@@ -2,6 +2,7 @@ import {shallow} from 'enzyme';
import React from 'react';
import {Player} from './Player';
import {callAPI} from '../../utils/Api';
+import GlobalInfos from "../../utils/GlobalInfos";
describe('', function () {
@@ -84,23 +85,54 @@ describe('', function () {
expect(wrapper.find('AddTagPopup')).toHaveLength(1);
});
- it('test delete button', done => {
+ it('test fully delete popup rendering', function () {
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');
- process.nextTick(() => {
- // refetch is called so fetch called 3 times
- expect(global.fetch).toHaveBeenCalledTimes(1);
- expect(wrapper.instance().props.history.goBack).toHaveBeenCalledTimes(1);
+ // click the first submit button
+ wrapper.find('ButtonPopup').dive().find('Button').at(0).simulate('click')
- global.fetch.mockClear();
- done();
+ // refetch is called so fetch called 3 times
+ 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('', function () {
it('test click of quickadd tag btn', done => {
const wrapper = generatetag();
- global.fetch = prepareFetchApi({result: 'success'});
+ callAPIMock({result: 'success'})
// render tag subcomponent
const tag = wrapper.find('Tag').first().dive();
tag.simulate('click');
process.nextTick(() => {
- expect(global.fetch).toHaveBeenCalledTimes(1);
-
- global.fetch.mockClear();
+ expect(callAPI).toHaveBeenCalledTimes(1);
done();
});
});
@@ -169,7 +199,7 @@ describe('', function () {
it('test failing quickadd', done => {
const wrapper = generatetag();
- global.fetch = prepareFetchApi({result: 'nonsuccess'});
+ callAPIMock({result: 'nonsuccess'});
global.console.error = jest.fn();
// render tag subcomponent
@@ -178,8 +208,6 @@ describe('', function () {
process.nextTick(() => {
expect(global.console.error).toHaveBeenCalledTimes(2);
-
- global.fetch.mockClear();
done();
});
});
diff --git a/src/pages/Player/Player.tsx b/src/pages/Player/Player.tsx
index ed4e27e..d777d84 100644
--- a/src/pages/Player/Player.tsx
+++ b/src/pages/Player/Player.tsx
@@ -21,6 +21,7 @@ import PlyrJS from 'plyr';
import {Button} from '../../elements/GPElements/Button';
import {VideoTypes} from '../../types/ApiTypes';
import GlobalInfos from '../../utils/GlobalInfos';
+import {ButtonPopup} from '../../elements/Popups/ButtonPopup/ButtonPopup';
interface Props extends RouteComponentProps<{id: string}> {}
@@ -35,6 +36,7 @@ interface mystate {
suggesttag: TagType[];
popupvisible: boolean;
actorpopupvisible: boolean;
+ deletepopupvisible: boolean;
actors: ActorType[];
}
@@ -56,6 +58,7 @@ export class Player extends React.Component {
suggesttag: [],
popupvisible: false,
actorpopupvisible: false,
+ deletepopupvisible: false,
actors: []
};
@@ -91,7 +94,7 @@ export class Player extends React.Component {
{
- this.deleteVideo();
+ this.setState({deletepopupvisible: true});
}}
color={{backgroundColor: 'red'}}
/>
@@ -196,10 +199,46 @@ export class Player extends React.Component {
movieId={this.state.movieId}
/>
) : null}
+ {this.state.deletepopupvisible ? this.renderDeletePopup() : null}
>
);
}
+ renderDeletePopup(): JSX.Element {
+ if (GlobalInfos.isVideoFulldeleteable()) {
+ return (
+ 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 (
+ 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
* @param tagId id of tag to add
@@ -325,17 +364,16 @@ export class Player extends React.Component {
/**
* delete the current video and return to last page
*/
- deleteVideo(): void {
+ deleteVideo(fullyDelete: boolean): void {
callAPI(
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) => {
if (result.result === 'success') {
// return to last element if successful
this.props.history.goBack();
} else {
- console.error('an error occured while liking');
- console.error(result);
+ console.error('an error occured while deleting the video: ' + JSON.stringify(result));
}
}
);
diff --git a/src/types/ApiTypes.ts b/src/types/ApiTypes.ts
index 69b804a..3f5a26f 100644
--- a/src/types/ApiTypes.ts
+++ b/src/types/ApiTypes.ts
@@ -37,6 +37,7 @@ export namespace SettingsTypes {
VideoPath: string;
TVShowPath: string;
TVShowEnabled: boolean;
+ FullDeleteEnabled: boolean;
}
export interface SettingsType {
diff --git a/src/utils/GlobalInfos.ts b/src/utils/GlobalInfos.ts
index c3a588b..170a479 100644
--- a/src/utils/GlobalInfos.ts
+++ b/src/utils/GlobalInfos.ts
@@ -10,6 +10,7 @@ class StaticInfos {
private videopath: string = '';
private tvshowpath: string = '';
private TVShowsEnabled: boolean = false;
+ private fullDeleteable: boolean = false;
/**
* check if the current theme is the dark theme
@@ -80,6 +81,14 @@ class StaticInfos {
isTVShowEnabled(): boolean {
return this.TVShowsEnabled;
}
+
+ setFullDeleteEnabled(FullDeleteEnabled: boolean): void {
+ this.fullDeleteable = FullDeleteEnabled;
+ }
+
+ isVideoFulldeleteable(): boolean {
+ return this.fullDeleteable;
+ }
}
export default new StaticInfos();