Compare commits
	
		
			1 Commits
		
	
	
		
			master
			...
			newPreview
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| f264faff31 | 
| @@ -45,8 +45,6 @@ module.exports = { | ||||
|  | ||||
|     // Map from global var to bool specifying if it can be redefined | ||||
|     globals: { | ||||
|         File: true, | ||||
|         FileList: true, | ||||
|         jest: true, | ||||
|         __DEV__: true, | ||||
|         __dirname: false, | ||||
| @@ -99,7 +97,6 @@ module.exports = { | ||||
|     rules: { | ||||
|         "@typescript-eslint/no-explicit-any": "error", | ||||
|         "@typescript-eslint/explicit-function-return-type": "error", | ||||
|         "@typescript-eslint/no-shadow": "warn", | ||||
|  | ||||
|         // General | ||||
|         'comma-dangle': [1, 'never'], // allow or disallow trailing commas | ||||
| @@ -185,7 +182,7 @@ module.exports = { | ||||
|         'no-catch-shadow': 1, // disallow the catch clause parameter name being the same as a variable in the outer scope (off by default in the node environment) | ||||
|         'no-delete-var': 1, // disallow deletion of variables | ||||
|         'no-label-var': 1, // disallow labels that share a name with a variable | ||||
|         // 'no-shadow': 1, // disallow declaration of variables already declared in the outer scope | ||||
|         'no-shadow': 1, // disallow declaration of variables already declared in the outer scope | ||||
|         'no-shadow-restricted-names': 1, // disallow shadowing of names such as arguments | ||||
|         'no-undef': 2, // disallow use of undeclared variables unless mentioned in a /*global */ block | ||||
|         'no-undefined': 0, // disallow use of undefined variable (off by default) | ||||
|   | ||||
| @@ -25,11 +25,11 @@ Minimize_Frontend: | ||||
|       - node_modules/ | ||||
|  | ||||
| Build_Backend: | ||||
|   image: luki42/go-ffmpeg:latest | ||||
|   image: golang:latest | ||||
|   stage: build_backend | ||||
|   script: | ||||
|     - cd apiGo | ||||
|     - go build -v -tags sharedffmpeg -o openmediacenter | ||||
|     - go build -v -o openmediacenter | ||||
|     - cp -r ../build/ ./static/ | ||||
|     - go build -v -tags static -o openmediacenter_full | ||||
|     - env GOOS=windows GOARCH=amd64 go build -v -tags static -o openmediacenter.exe | ||||
| @@ -62,7 +62,7 @@ Backend_Tests: | ||||
|   stage: test | ||||
|   script: | ||||
|     - cd apiGo | ||||
|     - go install github.com/jstemmer/go-junit-report@v0.9.1 | ||||
|     - go get -u github.com/jstemmer/go-junit-report | ||||
|     - go test -v ./... 2>&1 | go-junit-report -set-exit-code > report.xml | ||||
|   needs: [] | ||||
|   artifacts: | ||||
| @@ -93,9 +93,11 @@ Debian_Server: | ||||
|     - vers=$(grep -Po '"version":.*?[^\\]",' package.json | grep -Po '[0-9]+\.[0-9]+\.[0-9]+') # parse the version out of package .json | ||||
|     - cd deb | ||||
|     - mkdir -p "./OpenMediaCenter/var/www/openmediacenter/videos/" | ||||
|     - mkdir -p "./OpenMediaCenter/tmp/" | ||||
|     - mkdir -p "./OpenMediaCenter/usr/bin/" | ||||
|     - cp -r ../build/* ./OpenMediaCenter/var/www/openmediacenter/ | ||||
|     - cp ../apiGo/openmediacenter ./OpenMediaCenter/usr/bin/ | ||||
|     - cp ../database.sql ./OpenMediaCenter/tmp/openmediacenter.sql | ||||
|     - 'echo "Version: ${vers}" >> ./OpenMediaCenter/DEBIAN/control' | ||||
|     - chmod -R 0775 * | ||||
|     - dpkg-deb --build OpenMediaCenter | ||||
| @@ -108,13 +110,15 @@ Debian_Server: | ||||
|       - Minimize_Frontend | ||||
|       - Build_Backend | ||||
|  | ||||
| .Test_Server_Common: | ||||
| Test_Server: | ||||
|   stage: deploy | ||||
|   image: luki42/ssh:latest | ||||
|   needs: | ||||
|     - Frontend_Tests | ||||
|     - Backend_Tests | ||||
|     - Debian_Server | ||||
|   only: | ||||
|     - master | ||||
|   script: | ||||
|     - eval $(ssh-agent -s) | ||||
|     - echo "$SSH_PRIVATE_KEY" | ssh-add - | ||||
| @@ -124,38 +128,3 @@ Debian_Server: | ||||
|     - ssh root@192.168.0.42 "DEBIAN_FRONTEND=noninteractive apt-get --reinstall  -y -qq install /tmp/OpenMediaCenter-*.deb && rm /tmp/OpenMediaCenter-*.deb" | ||||
|   allow_failure: true | ||||
|  | ||||
| Test_Server_CD: | ||||
|   extends: .Test_Server_Common | ||||
|   only: | ||||
|     refs: | ||||
|       - master | ||||
|  | ||||
| Test_Server_MANUAL: | ||||
|   extends: .Test_Server_Common | ||||
|   when: manual | ||||
|  | ||||
| .Test_Server_2_Common: | ||||
|   stage: deploy | ||||
|   image: luki42/ssh:latest | ||||
|   needs: | ||||
|     - Frontend_Tests | ||||
|     - Backend_Tests | ||||
|     - Debian_Server | ||||
|   script: | ||||
|     - eval $(ssh-agent -s) | ||||
|     - echo "$SSH_PRIVATE_KEY_2" | ssh-add - | ||||
|     - mkdir -p ~/.ssh | ||||
|     - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config' | ||||
|     - scp deb/OpenMediaCenter-*.deb root@192.168.0.44:/tmp/ | ||||
|     - ssh root@192.168.0.44 "DEBIAN_FRONTEND=noninteractive apt-get --reinstall  -y -qq install /tmp/OpenMediaCenter-*.deb && rm /tmp/OpenMediaCenter-*.deb" | ||||
|   allow_failure: true | ||||
|  | ||||
| Test_Server_2_CD: | ||||
|   extends: .Test_Server_2_Common | ||||
|   only: | ||||
|     refs: | ||||
|       - master | ||||
|  | ||||
| Test_Server_2_MANUAL: | ||||
|   extends: .Test_Server_2_Common | ||||
|   when: manual | ||||
|   | ||||
| @@ -1,10 +0,0 @@ | ||||
| package api | ||||
|  | ||||
| func AddHandlers() { | ||||
| 	addVideoHandlers() | ||||
| 	addSettingsHandlers() | ||||
| 	addTagHandlers() | ||||
| 	addActorsHandlers() | ||||
| 	addTvshowHandlers() | ||||
| 	addUploadHandler() | ||||
| } | ||||
| @@ -2,97 +2,42 @@ package api | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"openmediacenter/apiGo/api/api" | ||||
| 	"openmediacenter/apiGo/api/types" | ||||
| 	"openmediacenter/apiGo/database" | ||||
| ) | ||||
|  | ||||
| func addActorsHandlers() { | ||||
| func AddActorsHandlers() { | ||||
| 	saveActorsToDB() | ||||
| 	getActorsFromDB() | ||||
| } | ||||
|  | ||||
| func getActorsFromDB() { | ||||
| 	/** | ||||
| 	 * @api {post} /api/actor [getAllActors] | ||||
| 	 * @apiDescription Get all available Actors | ||||
| 	 * @apiName getAllActors | ||||
| 	 * @apiGroup Actor | ||||
| 	 * | ||||
| 	 * @apiSuccess {Object[]} . Array of Actors available | ||||
| 	 * @apiSuccess {uint32} .ActorId Actor Id | ||||
| 	 * @apiSuccess {string} .Name Actor Name | ||||
| 	 * @apiSuccess {string} .Thumbnail Portrait Thumbnail | ||||
| 	 */ | ||||
| 	api.AddHandler("getAllActors", api.ActorNode, api.PermUser, func(context api.Context) { | ||||
| 		query := "SELECT actor_id, name, thumbnail FROM actors ORDER BY name ASC" | ||||
| 		context.Json(readActorsFromResultset(database.Query(query))) | ||||
| 	AddHandler("getAllActors", ActorNode, nil, func() []byte { | ||||
| 		query := "SELECT actor_id, name, thumbnail FROM actors" | ||||
| 		return jsonify(readActorsFromResultset(database.Query(query))) | ||||
| 	}) | ||||
|  | ||||
| 	/** | ||||
| 	 * @api {post} /api/actor [getActorsOfVideo] | ||||
| 	 * @apiDescription Get all actors playing in one video | ||||
| 	 * @apiName getActorsOfVideo | ||||
| 	 * @apiGroup Actor | ||||
| 	 * | ||||
| 	 * @apiParam {int} MovieId ID of video | ||||
| 	 * | ||||
| 	 * @apiSuccess {Object[]} . Array of Actors available | ||||
| 	 * @apiSuccess {uint32} .ActorId Actor Id | ||||
| 	 * @apiSuccess {string} .Name Actor Name | ||||
| 	 * @apiSuccess {string} .Thumbnail Portrait Thumbnail | ||||
| 	 */ | ||||
| 	api.AddHandler("getActorsOfVideo", api.ActorNode, api.PermUser, func(context api.Context) { | ||||
| 		var args struct { | ||||
| 			MovieId int | ||||
| 		} | ||||
| 		err := api.DecodeRequest(context.GetRequest(), &args) | ||||
| 		if err != nil { | ||||
| 			context.Text("failed to decode request") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 	var gaov struct { | ||||
| 		MovieId int | ||||
| 	} | ||||
| 	AddHandler("getActorsOfVideo", ActorNode, &gaov, func() []byte { | ||||
| 		query := fmt.Sprintf(`SELECT a.actor_id, name, thumbnail FROM actors_videos | ||||
| 									JOIN actors a on actors_videos.actor_id = a.actor_id | ||||
| 									WHERE actors_videos.video_id=%d`, args.MovieId) | ||||
| 									WHERE actors_videos.video_id=%d`, gaov.MovieId) | ||||
|  | ||||
| 		context.Json(readActorsFromResultset(database.Query(query))) | ||||
| 		return jsonify(readActorsFromResultset(database.Query(query))) | ||||
| 	}) | ||||
|  | ||||
| 	/** | ||||
| 	 * @api {post} /api/actor [getActorInfo] | ||||
| 	 * @apiDescription Get all infos for an actor | ||||
| 	 * @apiName getActorInfo | ||||
| 	 * @apiGroup Actor | ||||
| 	 * | ||||
| 	 * @apiParam {int} ActorId ID of Actor | ||||
| 	 * | ||||
| 	 * @apiSuccess {VideoUnloadedType[]} Videos Array of Videos this actor plays in | ||||
| 	 * @apiSuccess {uint32} Videos.MovieId Video Id | ||||
| 	 * @apiSuccess {string} Videos.MovieName Video Name | ||||
| 	 * | ||||
| 	 * @apiSuccess {Info} Info Infos about the actor | ||||
| 	 * @apiSuccess {uint32} Info.ActorId Actor Id | ||||
| 	 * @apiSuccess {string} Info.Name Actor Name | ||||
| 	 * @apiSuccess {string} Info.Thumbnail Actor Thumbnail | ||||
| 	 */ | ||||
| 	api.AddHandler("getActorInfo", api.ActorNode, api.PermUser, func(context api.Context) { | ||||
| 		var args struct { | ||||
| 			ActorId int | ||||
| 		} | ||||
|  | ||||
| 		err := api.DecodeRequest(context.GetRequest(), &args) | ||||
| 		if err != nil { | ||||
| 			context.Error("unable to decode request") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 	var gai struct { | ||||
| 		ActorId int | ||||
| 	} | ||||
| 	AddHandler("getActorInfo", ActorNode, &gai, func() []byte { | ||||
| 		query := fmt.Sprintf(`SELECT movie_id, movie_name FROM actors_videos | ||||
| 										JOIN videos v on v.movie_id = actors_videos.video_id | ||||
| 										WHERE actors_videos.actor_id=%d`, args.ActorId) | ||||
| 										WHERE actors_videos.actor_id=%d`, gai.ActorId) | ||||
| 		videos := readVideosFromResultset(database.Query(query)) | ||||
|  | ||||
| 		query = fmt.Sprintf("SELECT actor_id, name, thumbnail FROM actors WHERE actor_id=%d", args.ActorId) | ||||
| 		query = fmt.Sprintf("SELECT actor_id, name, thumbnail FROM actors WHERE actor_id=%d", gai.ActorId) | ||||
| 		actor := readActorsFromResultset(database.Query(query))[0] | ||||
|  | ||||
| 		var result = struct { | ||||
| @@ -103,55 +48,25 @@ func getActorsFromDB() { | ||||
| 			Info:   actor, | ||||
| 		} | ||||
|  | ||||
| 		context.Json(result) | ||||
| 		return jsonify(result) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func saveActorsToDB() { | ||||
| 	/** | ||||
| 	 * @api {post} /api/video [createActor] | ||||
| 	 * @apiDescription Create a new Actor | ||||
| 	 * @apiName createActor | ||||
| 	 * @apiGroup Actor | ||||
| 	 * | ||||
| 	 * @apiParam {string} ActorName Name of new Actor | ||||
| 	 * | ||||
| 	 * @apiSuccess {string} result 'success' if successfully or Error message if not | ||||
| 	 */ | ||||
| 	api.AddHandler("createActor", api.ActorNode, api.PermUser, func(context api.Context) { | ||||
| 		var args struct { | ||||
| 			ActorName string | ||||
| 		} | ||||
| 		api.DecodeRequest(context.GetRequest(), &args) | ||||
|  | ||||
| 	var ca struct { | ||||
| 		ActorName string | ||||
| 	} | ||||
| 	AddHandler("createActor", ActorNode, &ca, func() []byte { | ||||
| 		query := "INSERT IGNORE INTO actors (name) VALUES (?)" | ||||
| 		// todo bit ugly | ||||
| 		context.Text(string(database.SuccessQuery(query, args.ActorName))) | ||||
| 		return database.SuccessQuery(query, ca.ActorName) | ||||
| 	}) | ||||
|  | ||||
| 	/** | ||||
| 	 * @api {post} /api/video [addActorToVideo] | ||||
| 	 * @apiDescription Add Actor to Video | ||||
| 	 * @apiName addActorToVideo | ||||
| 	 * @apiGroup Actor | ||||
| 	 * | ||||
| 	 * @apiParam {int} ActorId Id of Actor | ||||
| 	 * @apiParam {int} MovieId Id of Movie to add to | ||||
| 	 * | ||||
| 	 * @apiSuccess {string} result 'success' if successfully or Error message if not | ||||
| 	 */ | ||||
| 	api.AddHandler("addActorToVideo", api.ActorNode, api.PermUser, func(context api.Context) { | ||||
| 		var args struct { | ||||
| 			ActorId int | ||||
| 			MovieId int | ||||
| 		} | ||||
| 		err := api.DecodeRequest(context.GetRequest(), &args) | ||||
| 		if err != nil { | ||||
| 			context.Error("unable to decode request") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		query := fmt.Sprintf("INSERT IGNORE INTO actors_videos (actor_id, video_id) VALUES (%d,%d)", args.ActorId, args.MovieId) | ||||
| 		context.Text(string(database.SuccessQuery(query))) | ||||
| 	var aatv struct { | ||||
| 		ActorId int | ||||
| 		MovieId int | ||||
| 	} | ||||
| 	AddHandler("addActorToVideo", ActorNode, &aatv, func() []byte { | ||||
| 		query := fmt.Sprintf("INSERT IGNORE INTO actors_videos (actor_id, video_id) VALUES (%d,%d)", aatv.ActorId, aatv.MovieId) | ||||
| 		return database.SuccessQuery(query) | ||||
| 	}) | ||||
| } | ||||
|   | ||||
							
								
								
									
										109
									
								
								apiGo/api/ApiBase.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								apiGo/api/ApiBase.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,109 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"openmediacenter/apiGo/api/oauth" | ||||
| ) | ||||
|  | ||||
| const APIPREFIX = "/api" | ||||
|  | ||||
| const ( | ||||
| 	VideoNode    = iota | ||||
| 	TagNode      = iota | ||||
| 	SettingsNode = iota | ||||
| 	ActorNode    = iota | ||||
| 	InitNode     = iota | ||||
| ) | ||||
|  | ||||
| type actionStruct struct { | ||||
| 	Action string | ||||
| } | ||||
|  | ||||
| type Handler struct { | ||||
| 	action    string | ||||
| 	handler   func() []byte | ||||
| 	arguments interface{} | ||||
| 	apiNode   int | ||||
| } | ||||
|  | ||||
| var handlers []Handler | ||||
|  | ||||
| func AddHandler(action string, apiNode int, n interface{}, h func() []byte) { | ||||
| 	// append new handler to the handlers | ||||
| 	handlers = append(handlers, Handler{action, h, n, apiNode}) | ||||
| } | ||||
|  | ||||
| func ServerInit() { | ||||
| 	http.Handle(APIPREFIX+"/video", oauth.ValidateToken(videoHandler)) | ||||
| 	http.Handle(APIPREFIX+"/tags", oauth.ValidateToken(tagHandler)) | ||||
| 	http.Handle(APIPREFIX+"/settings", oauth.ValidateToken(settingsHandler)) | ||||
| 	http.Handle(APIPREFIX+"/actor", oauth.ValidateToken(actorHandler)) | ||||
|  | ||||
| 	// initialization api calls to check if password is neccessaray | ||||
| 	http.Handle(APIPREFIX+"/init", http.HandlerFunc(initHandler)) | ||||
|  | ||||
| 	// initialize oauth service and add corresponding auth routes | ||||
| 	oauth.InitOAuth() | ||||
| } | ||||
|  | ||||
| func handleAPICall(action string, requestBody string, apiNode int) []byte { | ||||
| 	for i := range handlers { | ||||
| 		if handlers[i].action == action && handlers[i].apiNode == apiNode { | ||||
| 			// call the handler and return | ||||
|  | ||||
| 			if handlers[i].arguments != nil { | ||||
| 				// decode the arguments to the corresponding arguments object | ||||
| 				err := json.Unmarshal([]byte(requestBody), &handlers[i].arguments) | ||||
| 				if err != nil { | ||||
| 					fmt.Printf("failed to decode arguments of action %s :: %s\n", action, requestBody) | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			return handlers[i].handler() | ||||
| 		} | ||||
| 	} | ||||
| 	fmt.Printf("no handler found for Action: %d/%s\n", apiNode, action) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func actorHandler(rw http.ResponseWriter, req *http.Request) { | ||||
| 	handlefunc(rw, req, ActorNode) | ||||
| } | ||||
|  | ||||
| func videoHandler(rw http.ResponseWriter, req *http.Request) { | ||||
| 	handlefunc(rw, req, VideoNode) | ||||
| } | ||||
|  | ||||
| func tagHandler(rw http.ResponseWriter, req *http.Request) { | ||||
| 	handlefunc(rw, req, TagNode) | ||||
| } | ||||
|  | ||||
| func settingsHandler(rw http.ResponseWriter, req *http.Request) { | ||||
| 	handlefunc(rw, req, SettingsNode) | ||||
| } | ||||
|  | ||||
| func initHandler(rw http.ResponseWriter, req *http.Request) { | ||||
| 	handlefunc(rw, req, InitNode) | ||||
| } | ||||
|  | ||||
| func handlefunc(rw http.ResponseWriter, req *http.Request, node int) { | ||||
| 	// only allow post requests | ||||
| 	if req.Method != "POST" { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	buf := new(bytes.Buffer) | ||||
| 	buf.ReadFrom(req.Body) | ||||
| 	body := buf.String() | ||||
|  | ||||
| 	var t actionStruct | ||||
| 	err := json.Unmarshal([]byte(body), &t) | ||||
| 	if err != nil { | ||||
| 		fmt.Println("failed to read action from request! :: " + body) | ||||
| 	} | ||||
|  | ||||
| 	rw.Write(handleAPICall(t.Action, body, node)) | ||||
| } | ||||
							
								
								
									
										66
									
								
								apiGo/api/ApiBase_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								apiGo/api/ApiBase_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func cleanUp() { | ||||
| 	handlers = nil | ||||
| } | ||||
|  | ||||
| func TestAddHandler(t *testing.T) { | ||||
| 	cleanUp() | ||||
|  | ||||
| 	AddHandler("test", ActorNode, nil, func() []byte { | ||||
| 		return nil | ||||
| 	}) | ||||
| 	if len(handlers) != 1 { | ||||
| 		t.Errorf("Handler insertion failed, got: %d handlers, want: %d.", len(handlers), 1) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestCallOfHandler(t *testing.T) { | ||||
| 	cleanUp() | ||||
|  | ||||
| 	i := 0 | ||||
| 	AddHandler("test", ActorNode, nil, func() []byte { | ||||
| 		i++ | ||||
| 		return nil | ||||
| 	}) | ||||
|  | ||||
| 	// simulate the call of the api | ||||
| 	handleAPICall("test", "", ActorNode) | ||||
|  | ||||
| 	if i != 1 { | ||||
| 		t.Errorf("Unexpected number of Lambda calls : %d/1", i) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestDecodingOfArguments(t *testing.T) { | ||||
| 	cleanUp() | ||||
|  | ||||
| 	var myvar struct { | ||||
| 		Test    string | ||||
| 		TestInt int | ||||
| 	} | ||||
| 	AddHandler("test", ActorNode, &myvar, func() []byte { | ||||
| 		return nil | ||||
| 	}) | ||||
|  | ||||
| 	// simulate the call of the api | ||||
| 	handleAPICall("test", `{"Test":"myString","TestInt":42}`, ActorNode) | ||||
|  | ||||
| 	if myvar.TestInt != 42 || myvar.Test != "myString" { | ||||
| 		t.Errorf("Wrong parsing of argument parameters : %d/42 - %s/myString", myvar.TestInt, myvar.Test) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestNoHandlerCovers(t *testing.T) { | ||||
| 	cleanUp() | ||||
|  | ||||
| 	ret := handleAPICall("test", "", ActorNode) | ||||
|  | ||||
| 	if ret != nil { | ||||
| 		t.Error("Expect nil return within unhandled api action") | ||||
| 	} | ||||
| } | ||||
| @@ -1,69 +0,0 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"openmediacenter/apiGo/api/api" | ||||
| 	"openmediacenter/apiGo/database" | ||||
| 	"openmediacenter/apiGo/videoparser" | ||||
| 	"os" | ||||
| ) | ||||
|  | ||||
| func addUploadHandler() { | ||||
| 	api.AddHandler("fileupload", api.VideoNode, api.PermUser, func(ctx api.Context) { | ||||
| 		// get path where to store videos to | ||||
| 		mSettings, PathPrefix, _ := database.GetSettings() | ||||
|  | ||||
| 		req := ctx.GetRequest() | ||||
|  | ||||
| 		mr, err := req.MultipartReader() | ||||
| 		if err != nil { | ||||
| 			ctx.Errorf("incorrect request!") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		videoparser.InitDeps(&mSettings) | ||||
|  | ||||
| 		for { | ||||
| 			part, err := mr.NextPart() | ||||
| 			if err == io.EOF { | ||||
| 				break | ||||
| 			} | ||||
|  | ||||
| 			// only allow valid extensions | ||||
| 			if !videoparser.ValidVideoSuffix(part.FileName()) { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			vidpath := PathPrefix + mSettings.VideoPath + part.FileName() | ||||
| 			dst, err := os.OpenFile(vidpath, os.O_WRONLY|os.O_CREATE, 0644) | ||||
| 			if err != nil { | ||||
| 				ctx.Error("error opening file") | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			fmt.Printf("Uploading file %s\n", part.FileName()) | ||||
|  | ||||
| 			// so now loop through every appended file and upload | ||||
| 			buffer := make([]byte, 100000) | ||||
| 			for { | ||||
| 				cBytes, err := part.Read(buffer) | ||||
| 				if cBytes > 0 { | ||||
| 					dst.Write(buffer[0:cBytes]) | ||||
| 				} | ||||
|  | ||||
| 				if err == io.EOF { | ||||
| 					fmt.Printf("Finished uploading file %s\n", part.FileName()) | ||||
| 					go videoparser.ProcessVideo(part.FileName()) | ||||
| 					break | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			_ = dst.Close() | ||||
| 		} | ||||
|  | ||||
| 		ctx.Json(struct { | ||||
| 			Message string | ||||
| 		}{Message: "finished all files"}) | ||||
| 	}) | ||||
| } | ||||
| @@ -2,6 +2,7 @@ package api | ||||
|  | ||||
| import ( | ||||
| 	"database/sql" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"openmediacenter/apiGo/api/types" | ||||
| ) | ||||
| @@ -13,8 +14,7 @@ func readVideosFromResultset(rows *sql.Rows) []types.VideoUnloadedType { | ||||
| 		var vid types.VideoUnloadedType | ||||
| 		err := rows.Scan(&vid.MovieId, &vid.MovieName) | ||||
| 		if err != nil { | ||||
| 			fmt.Println(err.Error()) | ||||
| 			return nil | ||||
| 			panic(err.Error()) // proper error handling instead of panic in your app | ||||
| 		} | ||||
| 		result = append(result, vid) | ||||
| 	} | ||||
| @@ -31,7 +31,7 @@ func readTagsFromResultset(rows *sql.Rows) []types.Tag { | ||||
| 		var tag types.Tag | ||||
| 		err := rows.Scan(&tag.TagId, &tag.TagName) | ||||
| 		if err != nil { | ||||
| 			panic(err.Error()) // proper Error handling instead of panic in your app | ||||
| 			panic(err.Error()) // proper error handling instead of panic in your app | ||||
| 		} | ||||
| 		result = append(result, tag) | ||||
| 	} | ||||
| @@ -52,7 +52,7 @@ func readActorsFromResultset(rows *sql.Rows) []types.Actor { | ||||
| 			actor.Thumbnail = string(thumbnail) | ||||
| 		} | ||||
| 		if err != nil { | ||||
| 			panic(err.Error()) // proper Error handling instead of panic in your app | ||||
| 			panic(err.Error()) // proper error handling instead of panic in your app | ||||
| 		} | ||||
| 		result = append(result, actor) | ||||
| 	} | ||||
| @@ -61,18 +61,11 @@ func readActorsFromResultset(rows *sql.Rows) []types.Actor { | ||||
| 	return result | ||||
| } | ||||
|  | ||||
| // ID - Name : pay attention to the order! | ||||
| func readTVshowsFromResultset(rows *sql.Rows) []types.TVShow { | ||||
| 	result := []types.TVShow{} | ||||
| 	for rows.Next() { | ||||
| 		var vid types.TVShow | ||||
| 		err := rows.Scan(&vid.Id, &vid.Name) | ||||
| 		if err != nil { | ||||
| 			panic(err.Error()) // proper Error handling instead of panic in your app | ||||
| 		} | ||||
| 		result = append(result, vid) | ||||
| func jsonify(v interface{}) []byte { | ||||
| 	// jsonify results | ||||
| 	str, err := json.Marshal(v) | ||||
| 	if err != nil { | ||||
| 		fmt.Println("Error while Jsonifying return object: " + err.Error()) | ||||
| 	} | ||||
| 	rows.Close() | ||||
|  | ||||
| 	return result | ||||
| 	return str | ||||
| } | ||||
|   | ||||
							
								
								
									
										39
									
								
								apiGo/api/Init.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								apiGo/api/Init.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"openmediacenter/apiGo/database/settings" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| func AddInitHandlers() { | ||||
| 	passwordNeeded() | ||||
| } | ||||
|  | ||||
| func passwordNeeded() { | ||||
| 	AddHandler("loadInitialData", InitNode, nil, func() []byte { | ||||
| 		sett := settings.LoadSettings() | ||||
|  | ||||
| 		type InitialDataTypeResponse struct { | ||||
| 			DarkMode        bool | ||||
| 			Pasword         bool | ||||
| 			MediacenterName string | ||||
| 			VideoPath       string | ||||
| 		} | ||||
|  | ||||
| 		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}") | ||||
| 		videoUrl := regexMatchUrl.FindString(sett.VideoPath) | ||||
| 		serverVideoPath := strings.TrimPrefix(sett.VideoPath, videoUrl) | ||||
|  | ||||
| 		res := InitialDataTypeResponse{ | ||||
| 			DarkMode:        sett.DarkMode, | ||||
| 			Pasword:         sett.Pasword != "-1", | ||||
| 			MediacenterName: sett.Mediacenter_name, | ||||
| 			VideoPath:       serverVideoPath, | ||||
| 		} | ||||
|  | ||||
| 		str, _ := json.Marshal(res) | ||||
| 		return str | ||||
| 	}) | ||||
| } | ||||
| @@ -1,125 +1,29 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"openmediacenter/apiGo/api/api" | ||||
| 	"openmediacenter/apiGo/api/types" | ||||
| 	"openmediacenter/apiGo/config" | ||||
| 	"openmediacenter/apiGo/database" | ||||
| 	"openmediacenter/apiGo/database/settings" | ||||
| 	"openmediacenter/apiGo/videoparser" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| func addSettingsHandlers() { | ||||
| func AddSettingsHandlers() { | ||||
| 	saveSettingsToDB() | ||||
| 	getSettingsFromDB() | ||||
| 	reIndexHandling() | ||||
| } | ||||
|  | ||||
| func getSettingsFromDB() { | ||||
| 	/** | ||||
| 	 * @api {post} /api/settings [loadGeneralSettings] | ||||
| 	 * @apiDescription Get the settings object | ||||
| 	 * @apiName loadGeneralSettings | ||||
| 	 * @apiGroup Settings | ||||
| 	 * | ||||
| 	 * @apiSuccess {Object} Settings Settings object | ||||
| 	 * @apiSuccess {string} Settings.VideoPath webserver path to the videos | ||||
| 	 * @apiSuccess {string} Settings.EpisodePath webserver path to the tvshows | ||||
| 	 * @apiSuccess {string} Settings.MediacenterName overall name of the mediacenter | ||||
| 	 * @apiSuccess {string} Settings.Password new server password (-1 if no password set) | ||||
| 	 * @apiSuccess {bool} Settings.TMDBGrabbing TMDB grabbing support to grab tag info and thumbnails | ||||
| 	 * @apiSuccess {bool} Settings.DarkMode Darkmode enabled? | ||||
| 	 * @apiSuccess {Object} Sizes Sizes object | ||||
| 	 * @apiSuccess {uint32} Sizes.VideoNr total number of videos | ||||
| 	 * @apiSuccess {float32} Sizes.DBSize total size of database | ||||
| 	 * @apiSuccess {uint32} Sizes.DifferentTags number of different tags available | ||||
| 	 * @apiSuccess {uint32} Sizes.TagsAdded number of different tags added to videos | ||||
| 	 */ | ||||
| 	api.AddHandler("loadGeneralSettings", api.SettingsNode, api.PermUser, func(context api.Context) { | ||||
| 		result, _, sizes := database.GetSettings() | ||||
|  | ||||
| 		var ret = struct { | ||||
| 			Settings *types.SettingsType | ||||
| 			Sizes    *types.SettingsSizeType | ||||
| 		}{ | ||||
| 			Settings: &result, | ||||
| 			Sizes:    &sizes, | ||||
| 		} | ||||
| 		context.Json(ret) | ||||
| 	}) | ||||
|  | ||||
| 	/** | ||||
| 	 * @api {post} /api/settings [loadInitialData] | ||||
| 	 * @apiDescription load startdata to display on homepage | ||||
| 	 * @apiName loadInitialData | ||||
| 	 * @apiGroup Settings | ||||
| 	 * | ||||
| 	 * @apiSuccess {string} VideoPath webserver path to the videos | ||||
| 	 * @apiSuccess {string} EpisodePath webserver path to the tvshows | ||||
| 	 * @apiSuccess {string} MediacenterName overall name of the mediacenter | ||||
| 	 * @apiSuccess {string} Pasword new server password (-1 if no password set) | ||||
| 	 * @apiSuccess {bool} DarkMode Darkmode enabled? | ||||
| 	 * @apiSuccess {bool} TVShowEnabled is are TVShows enabled | ||||
| 	 */ | ||||
| 	api.AddHandler("loadInitialData", api.SettingsNode, api.PermUser, func(context api.Context) { | ||||
| 		sett := settings.LoadSettings() | ||||
|  | ||||
| 		type InitialDataTypeResponse struct { | ||||
| 			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}") | ||||
| 		videoUrl := regexMatchUrl.FindString(sett.VideoPath) | ||||
| 		tvshowurl := regexMatchUrl.FindString(sett.TVShowPath) | ||||
| 		serverVideoPath := strings.TrimPrefix(sett.VideoPath, videoUrl) | ||||
| 		serverTVShowPath := strings.TrimPrefix(sett.TVShowPath, tvshowurl) | ||||
|  | ||||
| 		res := InitialDataTypeResponse{ | ||||
| 			DarkMode:          sett.DarkMode, | ||||
| 			Pasword:           sett.Pasword != "-1", | ||||
| 			MediacenterName:   sett.MediacenterName, | ||||
| 			VideoPath:         serverVideoPath, | ||||
| 			TVShowPath:        serverTVShowPath, | ||||
| 			TVShowEnabled:     !config.GetConfig().Features.DisableTVSupport, | ||||
| 			FullDeleteEnabled: config.GetConfig().Features.FullyDeletableVideos, | ||||
| 		} | ||||
|  | ||||
| 		context.Json(res) | ||||
| 	AddHandler("loadGeneralSettings", SettingsNode, nil, func() []byte { | ||||
| 		result := database.GetSettings() | ||||
| 		return jsonify(result) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func saveSettingsToDB() { | ||||
| 	/** | ||||
| 	 * @api {post} /api/settings [saveGeneralSettings] | ||||
| 	 * @apiDescription Save the global settings provided | ||||
| 	 * @apiName saveGeneralSettings | ||||
| 	 * @apiGroup Settings | ||||
| 	 * | ||||
| 	 * @apiParam {string} VideoPath webserver path to the videos | ||||
| 	 * @apiParam {string} EpisodePath webserver path to the tvshows | ||||
| 	 * @apiParam {string} MediacenterName overall name of the mediacenter | ||||
| 	 * @apiParam {string} Password new server password (-1 if no password set) | ||||
| 	 * @apiParam {bool} TMDBGrabbing TMDB grabbing support to grab tag info and thumbnails | ||||
| 	 * @apiParam {bool} DarkMode Darkmode enabled? | ||||
| 	 * | ||||
| 	 * @apiSuccess {string} result 'success' if successfully or Error message if not | ||||
| 	 */ | ||||
| 	api.AddHandler("saveGeneralSettings", api.SettingsNode, api.PermUser, func(context api.Context) { | ||||
| 		var args types.SettingsType | ||||
| 		err := api.DecodeRequest(context.GetRequest(), &args) | ||||
| 		if err != nil { | ||||
| 			context.Error("unable to decode arguments") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 	var sgs struct { | ||||
| 		Settings types.SettingsType | ||||
| 	} | ||||
| 	AddHandler("saveGeneralSettings", SettingsNode, &sgs, func() []byte { | ||||
| 		query := ` | ||||
| 					UPDATE settings SET  | ||||
|                         video_path=?, | ||||
| @@ -129,48 +33,25 @@ func saveSettingsToDB() { | ||||
|                         TMDB_grabbing=?,  | ||||
|                         DarkMode=? | ||||
|                     WHERE 1` | ||||
| 		// todo avoid conversion | ||||
| 		context.Text(string(database.SuccessQuery(query, | ||||
| 			args.VideoPath, args.EpisodePath, args.Password, | ||||
| 			args.MediacenterName, args.TMDBGrabbing, args.DarkMode))) | ||||
| 		return database.SuccessQuery(query, | ||||
| 			sgs.Settings.VideoPath, sgs.Settings.EpisodePath, sgs.Settings.Password, | ||||
| 			sgs.Settings.MediacenterName, sgs.Settings.TMDBGrabbing, sgs.Settings.DarkMode) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // methods for handling reindexing and cleanup of db gravity | ||||
| func reIndexHandling() { | ||||
| 	/** | ||||
| 	 * @api {post} /api/settings [startReindex] | ||||
| 	 * @apiDescription Start Database video reindex Job | ||||
| 	 * @apiName startReindex | ||||
| 	 * @apiGroup Settings | ||||
| 	 * | ||||
| 	 * @apiSuccess {string} result 'success' if successfully or Error message if not | ||||
| 	 */ | ||||
| 	api.AddHandler("startReindex", api.SettingsNode, api.PermUser, func(context api.Context) { | ||||
| 	AddHandler("startReindex", SettingsNode, nil, func() []byte { | ||||
| 		videoparser.StartReindex() | ||||
| 		context.Text(string(database.ManualSuccessResponse(nil))) | ||||
| 		return database.ManualSuccessResponse(nil) | ||||
| 	}) | ||||
|  | ||||
| 	/** | ||||
| 	 * @api {post} /api/settings [startTVShowReindex] | ||||
| 	 * @apiDescription Start Database TVShow reindex job | ||||
| 	 * @apiName startTVShowReindex | ||||
| 	 * @apiGroup Settings | ||||
| 	 * | ||||
| 	 * @apiSuccess {string} result 'success' if successfully or Error message if not | ||||
| 	 */ | ||||
| 	api.AddHandler("startTVShowReindex", api.SettingsNode, api.PermUser, func(context api.Context) { | ||||
| 		videoparser.StartTVShowReindex() | ||||
| 		context.Text(string(database.ManualSuccessResponse(nil))) | ||||
| 	}) | ||||
|  | ||||
| 	/** | ||||
| 	 * @api {post} /api/settings [cleanupGravity] | ||||
| 	 * @apiDescription Start Database cleanup job | ||||
| 	 * @apiName cleanupGravity | ||||
| 	 * @apiGroup Settings | ||||
| 	 */ | ||||
| 	api.AddHandler("cleanupGravity", api.SettingsNode, api.PermUser, func(context api.Context) { | ||||
| 	AddHandler("cleanupGravity", SettingsNode, nil, func() []byte { | ||||
| 		videoparser.StartCleanup() | ||||
| 		return nil | ||||
| 	}) | ||||
|  | ||||
| 	AddHandler("getStatusMessage", SettingsNode, nil, func() []byte { | ||||
| 		return jsonify(videoparser.GetStatusMessage()) | ||||
| 	}) | ||||
| } | ||||
|   | ||||
| @@ -1,166 +0,0 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"openmediacenter/apiGo/api/api" | ||||
| 	"openmediacenter/apiGo/config" | ||||
| 	"openmediacenter/apiGo/database" | ||||
| ) | ||||
|  | ||||
| func addTvshowHandlers() { | ||||
| 	// do not add handlers if tvshows not enabled | ||||
| 	if config.GetConfig().Features.DisableTVSupport { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * @api {post} /api/tvshow [getTVShows] | ||||
| 	 * @apiDescription get all available tv shows | ||||
| 	 * @apiName getTVShows | ||||
| 	 * @apiGroup TVshow | ||||
| 	 * | ||||
| 	 * @apiSuccess {Object[]} . | ||||
| 	 * @apiSuccess {uint32} .Id tvshow id | ||||
| 	 * @apiSuccess {string} .Name tvshow name | ||||
| 	 */ | ||||
| 	api.AddHandler("getTVShows", api.TVShowNode, api.PermUser, func(context api.Context) { | ||||
| 		query := "SELECT id, name FROM tvshow" | ||||
| 		rows := database.Query(query) | ||||
| 		context.Json(readTVshowsFromResultset(rows)) | ||||
| 	}) | ||||
|  | ||||
| 	/** | ||||
| 	 * @api {post} /api/tvshow [getEpisodes] | ||||
| 	 * @apiDescription get all Episodes of a TVShow | ||||
| 	 * @apiName getEpisodes | ||||
| 	 * @apiGroup TVshow | ||||
| 	 * | ||||
| 	 * @apiParam {uint32} ShowID id of tvshow to get episodes from | ||||
| 	 * | ||||
| 	 * @apiSuccess {Object[]} . | ||||
| 	 * @apiSuccess {uint32} .ID episode id | ||||
| 	 * @apiSuccess {string} .Name episode name | ||||
| 	 * @apiSuccess {uint8} .Season Season number | ||||
| 	 * @apiSuccess {uint8} .Episode Episode number | ||||
| 	 */ | ||||
| 	api.AddHandler("getEpisodes", api.TVShowNode, api.PermUser, func(context api.Context) { | ||||
| 		var args struct { | ||||
| 			ShowID uint32 | ||||
| 		} | ||||
| 		err := api.DecodeRequest(context.GetRequest(), &args) | ||||
| 		if err != nil { | ||||
| 			context.Text("unable to decode request") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		query := fmt.Sprintf("SELECT id, name, season, episode FROM tvshow_episodes WHERE tvshow_id=%d", args.ShowID) | ||||
| 		rows := database.Query(query) | ||||
|  | ||||
| 		type Episode struct { | ||||
| 			ID      uint32 | ||||
| 			Name    string | ||||
| 			Season  uint8 | ||||
| 			Episode uint8 | ||||
| 		} | ||||
|  | ||||
| 		episodes := []Episode{} | ||||
| 		for rows.Next() { | ||||
| 			var ep Episode | ||||
| 			err := rows.Scan(&ep.ID, &ep.Name, &ep.Season, &ep.Episode) | ||||
| 			if err != nil { | ||||
| 				fmt.Println(err.Error()) | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			episodes = append(episodes, ep) | ||||
| 		} | ||||
|  | ||||
| 		context.Json(episodes) | ||||
| 	}) | ||||
|  | ||||
| 	/** | ||||
| 	 * @api {post} /api/tvshow [loadEpisode] | ||||
| 	 * @apiDescription load all info of episode | ||||
| 	 * @apiName loadEpisode | ||||
| 	 * @apiGroup TVshow | ||||
| 	 * | ||||
| 	 * @apiParam {uint32} ID id of episode | ||||
| 	 * | ||||
| 	 * @apiSuccess {uint32} TVShowID episode id | ||||
| 	 * @apiSuccess {string} Name episode name | ||||
| 	 * @apiSuccess {uint8} Season Season number | ||||
| 	 * @apiSuccess {uint8} Episode Episode number | ||||
| 	 * @apiSuccess {string} Path webserver path of video file | ||||
| 	 */ | ||||
| 	api.AddHandler("loadEpisode", api.TVShowNode, api.PermUser, func(context api.Context) { | ||||
| 		var args struct { | ||||
| 			ID uint32 | ||||
| 		} | ||||
| 		err := api.DecodeRequest(context.GetRequest(), &args) | ||||
| 		if err != nil { | ||||
| 			context.Text("unable to decode argument") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		query := fmt.Sprintf(` | ||||
| SELECT tvshow_episodes.name, season, tvshow_id, episode, filename, t.foldername | ||||
| FROM tvshow_episodes  | ||||
| JOIN tvshow t on t.id = tvshow_episodes.tvshow_id | ||||
| WHERE tvshow_episodes.id=%d`, args.ID) | ||||
| 		row := database.QueryRow(query) | ||||
|  | ||||
| 		var ret struct { | ||||
| 			Name     string | ||||
| 			Season   uint8 | ||||
| 			Episode  uint8 | ||||
| 			TVShowID uint32 | ||||
| 			Path     string | ||||
| 		} | ||||
| 		var filename string | ||||
| 		var foldername string | ||||
|  | ||||
| 		err = row.Scan(&ret.Name, &ret.Season, &ret.TVShowID, &ret.Episode, &filename, &foldername) | ||||
| 		if err != nil { | ||||
| 			fmt.Println(err.Error()) | ||||
| 			context.Error(err.Error()) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		ret.Path = foldername + "/" + filename | ||||
|  | ||||
| 		context.Json(ret) | ||||
| 	}) | ||||
|  | ||||
| 	/** | ||||
| 	 * @api {post} /api/tvshow [readThumbnail] | ||||
| 	 * @apiDescription Load Thubnail of specific episode | ||||
| 	 * @apiName readThumbnail | ||||
| 	 * @apiGroup TVshow | ||||
| 	 * | ||||
| 	 * @apiParam {int} Id id of episode to load thumbnail | ||||
| 	 * | ||||
| 	 * @apiSuccess {string} . Base64 encoded Thubnail | ||||
| 	 */ | ||||
| 	api.AddHandler("readThumbnail", api.TVShowNode, api.PermUser, func(context api.Context) { | ||||
| 		var args struct { | ||||
| 			Id int | ||||
| 		} | ||||
| 		err := api.DecodeRequest(context.GetRequest(), &args) | ||||
| 		if err != nil { | ||||
| 			context.Text("unable to decode request") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		var pic []byte | ||||
|  | ||||
| 		query := fmt.Sprintf("SELECT thumbnail FROM tvshow WHERE id=%d", args.Id) | ||||
|  | ||||
| 		err = database.QueryRow(query).Scan(&pic) | ||||
| 		if err != nil { | ||||
| 			fmt.Printf("the thumbnail of movie id %d couldn't be found", args.Id) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		context.Text(string(pic)) | ||||
| 	}) | ||||
| } | ||||
| @@ -2,135 +2,73 @@ package api | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"openmediacenter/apiGo/api/api" | ||||
| 	"openmediacenter/apiGo/database" | ||||
| 	"regexp" | ||||
| ) | ||||
|  | ||||
| func addTagHandlers() { | ||||
| func AddTagHandlers() { | ||||
| 	getFromDB() | ||||
| 	addToDB() | ||||
| 	deleteFromDB() | ||||
| } | ||||
|  | ||||
| func deleteFromDB() { | ||||
| 	/** | ||||
| 	 * @api {post} /api/tags [deleteTag] | ||||
| 	 * @apiDescription Start Database video reindex Job | ||||
| 	 * @apiName deleteTag | ||||
| 	 * @apiGroup Tags | ||||
| 	 * | ||||
| 	 * @apiParam {bool} [Force] force delete tag with its constraints | ||||
| 	 * @apiParam {int} TagId id of tag to delete | ||||
| 	 * | ||||
| 	 * @apiSuccess {string} result 'success' if successfully or Error message if not | ||||
| 	 */ | ||||
| 	api.AddHandler("deleteTag", api.TagNode, api.PermUser, func(context api.Context) { | ||||
| 		var args struct { | ||||
| 			TagId int | ||||
| 			Force bool | ||||
| 		} | ||||
| 		err := api.DecodeRequest(context.GetRequest(), &args) | ||||
| 		if err != nil { | ||||
| 			context.Text("unable to decode request") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 	var dT struct { | ||||
| 		TagId int | ||||
| 		Force bool | ||||
| 	} | ||||
| 	AddHandler("deleteTag", TagNode, &dT, func() []byte { | ||||
| 		// delete key constraints first | ||||
| 		if args.Force { | ||||
| 			query := fmt.Sprintf("DELETE FROM video_tags WHERE tag_id=%d", args.TagId) | ||||
| 		if dT.Force { | ||||
| 			query := fmt.Sprintf("DELETE FROM video_tags WHERE tag_id=%d", dT.TagId) | ||||
| 			err := database.Edit(query) | ||||
|  | ||||
| 			// respond only if result not successful | ||||
| 			if err != nil { | ||||
| 				context.Text(string(database.ManualSuccessResponse(err))) | ||||
| 				return | ||||
| 				return database.ManualSuccessResponse(err) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		query := fmt.Sprintf("DELETE FROM tags WHERE tag_id=%d", args.TagId) | ||||
| 		err = database.Edit(query) | ||||
| 		query := fmt.Sprintf("DELETE FROM tags WHERE tag_id=%d", dT.TagId) | ||||
| 		err := database.Edit(query) | ||||
|  | ||||
| 		if err == nil { | ||||
| 			// return if successful | ||||
| 			context.Text(string(database.ManualSuccessResponse(err))) | ||||
| 			return database.ManualSuccessResponse(err) | ||||
| 		} else { | ||||
| 			// check with regex if its the key constraint Error | ||||
| 			r := regexp.MustCompile("^.*a foreign key constraint fails.*$") | ||||
| 			// check with regex if its the key constraint error | ||||
| 			r, _ := regexp.Compile("^.*a foreign key constraint fails.*$") | ||||
| 			if r.MatchString(err.Error()) { | ||||
| 				context.Text(string(database.ManualSuccessResponse(fmt.Errorf("not empty tag")))) | ||||
| 				return []byte(`{"result":"not empty tag"}`) | ||||
| 			} else { | ||||
| 				context.Text(string(database.ManualSuccessResponse(err))) | ||||
| 				return database.ManualSuccessResponse(err) | ||||
| 			} | ||||
| 		} | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func getFromDB() { | ||||
| 	/** | ||||
| 	 * @api {post} /api/tags [getAllTags] | ||||
| 	 * @apiDescription get all available Tags | ||||
| 	 * @apiName getAllTags | ||||
| 	 * @apiGroup Tags | ||||
| 	 * | ||||
| 	 * @apiSuccess {Object[]} array of tag objects | ||||
| 	 * @apiSuccess {uint32} TagId | ||||
| 	 * @apiSuccess {string} TagName name of the Tag | ||||
| 	 */ | ||||
| 	api.AddHandler("getAllTags", api.TagNode, api.PermUser, func(context api.Context) { | ||||
| 		query := "SELECT tag_id,tag_name from tags ORDER BY tag_name ASC" | ||||
| 		context.Json(readTagsFromResultset(database.Query(query))) | ||||
| 	AddHandler("getAllTags", TagNode, nil, func() []byte { | ||||
| 		query := "SELECT tag_id,tag_name from tags" | ||||
| 		return jsonify(readTagsFromResultset(database.Query(query))) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func addToDB() { | ||||
| 	/** | ||||
| 	 * @api {post} /api/tags [createTag] | ||||
| 	 * @apiDescription create a new tag | ||||
| 	 * @apiName createTag | ||||
| 	 * @apiGroup Tags | ||||
| 	 * | ||||
| 	 * @apiParam {string} TagName name of the tag | ||||
| 	 * | ||||
| 	 * @apiSuccess {string} result 'success' if successfully or Error message if not | ||||
| 	 */ | ||||
| 	api.AddHandler("createTag", api.TagNode, api.PermUser, func(context api.Context) { | ||||
| 		var args struct { | ||||
| 			TagName string | ||||
| 		} | ||||
| 		err := api.DecodeRequest(context.GetRequest(), &args) | ||||
| 		if err != nil { | ||||
| 			context.Text("unable to decode request") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 	var ct struct { | ||||
| 		TagName string | ||||
| 	} | ||||
| 	AddHandler("createTag", TagNode, &ct, func() []byte { | ||||
| 		query := "INSERT IGNORE INTO tags (tag_name) VALUES (?)" | ||||
| 		context.Text(string(database.SuccessQuery(query, args.TagName))) | ||||
| 		return database.SuccessQuery(query, ct.TagName) | ||||
| 	}) | ||||
|  | ||||
| 	/** | ||||
| 	 * @api {post} /api/tags [addTag] | ||||
| 	 * @apiDescription Add new tag to video | ||||
| 	 * @apiName addTag | ||||
| 	 * @apiGroup Tags | ||||
| 	 * | ||||
| 	 * @apiParam {int} TagId Tag id to add to video | ||||
| 	 * @apiParam {int} MovieId Video Id of video to add tag to | ||||
| 	 * | ||||
| 	 * @apiSuccess {string} result 'success' if successfully or Error message if not | ||||
| 	 */ | ||||
| 	api.AddHandler("addTag", api.TagNode, api.PermUser, func(context api.Context) { | ||||
| 		var args struct { | ||||
| 			MovieId int | ||||
| 			TagId   int | ||||
| 		} | ||||
| 		err := api.DecodeRequest(context.GetRequest(), &args) | ||||
| 		if err != nil { | ||||
| 			context.Text("unable to decode request") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 	var at struct { | ||||
| 		MovieId int | ||||
| 		TagId   int | ||||
| 	} | ||||
| 	AddHandler("addTag", TagNode, &at, func() []byte { | ||||
| 		query := "INSERT IGNORE INTO video_tags(tag_id, video_id) VALUES (?,?)" | ||||
| 		context.Text(string(database.SuccessQuery(query, args.TagId, args.MovieId))) | ||||
| 		return database.SuccessQuery(query, at.TagId, at.MovieId) | ||||
| 	}) | ||||
| } | ||||
|   | ||||
| @@ -4,311 +4,134 @@ import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"net/url" | ||||
| 	"openmediacenter/apiGo/api/api" | ||||
| 	"openmediacenter/apiGo/api/types" | ||||
| 	"openmediacenter/apiGo/config" | ||||
| 	"openmediacenter/apiGo/database" | ||||
| 	"os" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| func addVideoHandlers() { | ||||
| func AddVideoHandlers() { | ||||
| 	getVideoHandlers() | ||||
| 	loadVideosHandlers() | ||||
| 	addToVideoHandlers() | ||||
| } | ||||
|  | ||||
| func getVideoHandlers() { | ||||
| 	/** | ||||
| 	 * @api {post} /api/video [getMovies] | ||||
| 	 * @apiDescription Request available Videos | ||||
| 	 * @apiName GetMovies | ||||
| 	 * @apiGroup video | ||||
| 	 * | ||||
| 	 * @apiParam {int} [Tag=1] id of VideoTag to get videos (1=all) | ||||
| 	 * | ||||
| 	 * @apiSuccess {Object[]} Videos List of Videos | ||||
| 	 * @apiSuccess {number} Videos.MovieId Id of Video | ||||
| 	 * @apiSuccess {String} Videos.MovieName  Name of video | ||||
| 	 * @apiSuccess {String} TagName Name of the Tag returned | ||||
| 	 */ | ||||
| 	api.AddHandler("getMovies", api.VideoNode, api.PermUser, func(context api.Context) { | ||||
| 		var args struct { | ||||
| 			Tag  uint32 | ||||
| 			Sort uint8 | ||||
| 		} | ||||
| 		err := api.DecodeRequest(context.GetRequest(), &args) | ||||
| 		if err != nil { | ||||
| 			context.Text("unable to decode request") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		const ( | ||||
| 			date   = iota | ||||
| 			likes  = iota | ||||
| 			random = iota | ||||
| 			names  = iota | ||||
| 			length = iota | ||||
| 		) | ||||
|  | ||||
| 		// if wrong number passed no sorting is performed | ||||
| 		var SortClause = "" | ||||
| 		switch args.Sort { | ||||
| 		case date: | ||||
| 			SortClause = "ORDER BY create_date DESC, movie_name" | ||||
| 			break | ||||
| 		case likes: | ||||
| 			SortClause = "ORDER BY likes DESC" | ||||
| 			break | ||||
| 		case random: | ||||
| 			SortClause = "ORDER BY RAND()" | ||||
| 			break | ||||
| 		case names: | ||||
| 			SortClause = "ORDER BY movie_name" | ||||
| 			break | ||||
| 		case length: | ||||
| 			SortClause = "ORDER BY length DESC" | ||||
| 			break | ||||
| 		} | ||||
|  | ||||
| 	var mrq struct { | ||||
| 		Tag int | ||||
| 	} | ||||
| 	AddHandler("getMovies", VideoNode, &mrq, func() []byte { | ||||
| 		var query string | ||||
| 		// 1 is the id of the ALL tag | ||||
| 		if args.Tag != 1 { | ||||
| 			query = fmt.Sprintf(`SELECT movie_id,movie_name,previewratio,t.tag_name FROM videos | ||||
| 		if mrq.Tag != 1 { | ||||
| 			query = fmt.Sprintf(`SELECT movie_id,movie_name FROM videos | ||||
| 					INNER JOIN video_tags vt on videos.movie_id = vt.video_id | ||||
| 					INNER JOIN tags t on vt.tag_id = t.tag_id | ||||
| 					WHERE t.tag_id = %d %s`, args.Tag, SortClause) | ||||
| 					WHERE t.tag_id = '%d' | ||||
| 					ORDER BY likes DESC, create_date, movie_name`, mrq.Tag) | ||||
| 		} else { | ||||
| 			query = fmt.Sprintf("SELECT movie_id,movie_name,previewratio, (SELECT 'All' as tag_name) FROM videos %s", SortClause) | ||||
| 			query = "SELECT movie_id,movie_name FROM videos ORDER BY create_date DESC, movie_name" | ||||
| 		} | ||||
|  | ||||
| 		var result struct { | ||||
| 			Videos  []types.VideoUnloadedType | ||||
| 			TagName string | ||||
| 		} | ||||
|  | ||||
| 		rows := database.Query(query) | ||||
| 		vids := []types.VideoUnloadedType{} | ||||
| 		var name string | ||||
| 		for rows.Next() { | ||||
| 			var vid types.VideoUnloadedType | ||||
| 			err := rows.Scan(&vid.MovieId, &vid.MovieName, &vid.Ratio, &name) | ||||
| 			if err != nil { | ||||
| 				return | ||||
| 			} | ||||
| 			vids = append(vids, vid) | ||||
| 		} | ||||
| 		if rows.Close() != nil { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		// if the tag id doesn't exist the query won't return a name | ||||
| 		if name == "" { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		result.Videos = vids | ||||
| 		result.TagName = name | ||||
| 		context.Json(result) | ||||
| 		result := readVideosFromResultset(database.Query(query)) | ||||
| 		// jsonify results | ||||
| 		str, _ := json.Marshal(result) | ||||
| 		return str | ||||
| 	}) | ||||
|  | ||||
| 	/** | ||||
| 	 * @api {post} /api/video [readThumbnail] | ||||
| 	 * @apiDescription Load Thubnail of specific Video | ||||
| 	 * @apiName readThumbnail | ||||
| 	 * @apiGroup video | ||||
| 	 * | ||||
| 	 * @apiParam {int} Movieid id of video to load thumbnail | ||||
| 	 * | ||||
| 	 * @apiSuccess {string} . Base64 encoded Thubnail | ||||
| 	 */ | ||||
| 	api.AddHandler("readThumbnail", api.VideoNode, api.PermUser, func(context api.Context) { | ||||
| 		var args struct { | ||||
| 			Movieid int | ||||
| 		} | ||||
| 		err := api.DecodeRequest(context.GetRequest(), &args) | ||||
| 		if err != nil { | ||||
| 			context.Text("unable to decode request") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 	var rtn struct { | ||||
| 		Movieid int | ||||
| 	} | ||||
| 	AddHandler("readThumbnail", VideoNode, &rtn, func() []byte { | ||||
| 		var pic []byte | ||||
|  | ||||
| 		query := fmt.Sprintf("SELECT thumbnail FROM videos WHERE movie_id=%d", args.Movieid) | ||||
| 		query := fmt.Sprintf("SELECT thumbnail FROM videos WHERE movie_id='%d'", rtn.Movieid) | ||||
|  | ||||
| 		err = database.QueryRow(query).Scan(&pic) | ||||
| 		err := database.QueryRow(query).Scan(&pic) | ||||
| 		if err != nil { | ||||
| 			fmt.Printf("the thumbnail of movie id %d couldn't be found", args.Movieid) | ||||
| 			return | ||||
| 			fmt.Printf("the thumbnail of movie id %d couldn't be found", rtn.Movieid) | ||||
| 			return nil | ||||
| 		} | ||||
|  | ||||
| 		context.Text(string(pic)) | ||||
| 		return pic | ||||
| 	}) | ||||
|  | ||||
| 	/** | ||||
| 	 * @api {post} /api/video [getRandomMovies] | ||||
| 	 * @apiDescription Load random videos | ||||
| 	 * @apiName getRandomMovies | ||||
| 	 * @apiGroup video | ||||
| 	 * | ||||
| 	 * @apiParam {int} Number number of random videos to load | ||||
| 	 * | ||||
| 	 * @apiSuccess {Object[]} Tags Array of tags occuring in selection | ||||
| 	 * @apiSuccess {string} Tags.TagName Tagname | ||||
| 	 * @apiSuccess {uint32} Tags.TagId Tag ID | ||||
| 	 * | ||||
| 	 * @apiSuccess {Object[]} Videos Array of the videos | ||||
| 	 * @apiSuccess {string} Videos.MovieName Video Name | ||||
| 	 * @apiSuccess {int} Videos.MovieId Video ID | ||||
| 	 */ | ||||
| 	api.AddHandler("getRandomMovies", api.VideoNode, api.PermUser, func(context api.Context) { | ||||
| 		var args struct { | ||||
| 			Number    int | ||||
| 			TagFilter []uint32 | ||||
| 		} | ||||
| 		if api.DecodeRequest(context.GetRequest(), &args) != nil { | ||||
| 			context.Text("unable to decode request") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 	var grm struct { | ||||
| 		Number int | ||||
| 	} | ||||
| 	AddHandler("getRandomMovies", VideoNode, &grm, func() []byte { | ||||
| 		var result struct { | ||||
| 			Tags   []types.Tag | ||||
| 			Videos []types.VideoUnloadedType | ||||
| 		} | ||||
|  | ||||
| 		whereclause := "WHERE 1" | ||||
| 		if len(args.TagFilter) > 0 { | ||||
| 			d, _ := json.Marshal(args.TagFilter) | ||||
| 			vals := strings.Trim(string(d), "[]") | ||||
|  | ||||
| 			whereclause = fmt.Sprintf("WHERE tag_id IN (%s)", vals) | ||||
| 		} | ||||
|  | ||||
| 		query := fmt.Sprintf(` | ||||
| SELECT video_tags.video_id,v.movie_name FROM video_tags join videos v on v.movie_id = video_tags.video_id | ||||
|                                         %s | ||||
|                                         group by video_id | ||||
|                                         ORDER BY RAND() | ||||
|                                         LIMIT %d`, whereclause, args.Number) | ||||
| 		query := fmt.Sprintf("SELECT movie_id,movie_name FROM videos ORDER BY RAND() LIMIT %d", grm.Number) | ||||
| 		result.Videos = readVideosFromResultset(database.Query(query)) | ||||
|  | ||||
| 		if len(result.Videos) > 0 { | ||||
| 			var ids string | ||||
| 			for i := range result.Videos { | ||||
| 				ids += "video_tags.video_id=" + strconv.Itoa(result.Videos[i].MovieId) | ||||
| 		var ids string | ||||
| 		for i := range result.Videos { | ||||
| 			ids += "video_tags.video_id=" + strconv.Itoa(result.Videos[i].MovieId) | ||||
|  | ||||
| 				if i < len(result.Videos)-1 { | ||||
| 					ids += " OR " | ||||
| 				} | ||||
| 			if i < len(result.Videos)-1 { | ||||
| 				ids += " OR " | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 			// add the corresponding tags | ||||
| 			query = fmt.Sprintf(`SELECT t.tag_name,t.tag_id FROM video_tags | ||||
| 		// add the corresponding tags | ||||
| 		query = fmt.Sprintf(`SELECT t.tag_name,t.tag_id FROM video_tags | ||||
| 									INNER JOIN tags t on video_tags.tag_id = t.tag_id | ||||
| 									WHERE %s | ||||
| 									GROUP BY t.tag_id`, ids) | ||||
|  | ||||
| 			rows := database.Query(query) | ||||
| 			if rows != nil { | ||||
| 				for rows.Next() { | ||||
| 					var tag types.Tag | ||||
| 					err := rows.Scan(&tag.TagName, &tag.TagId) | ||||
| 					if err != nil { | ||||
| 						panic(err.Error()) // proper Error handling instead of panic in your app | ||||
| 					} | ||||
| 					// append to final array | ||||
| 					result.Tags = append(result.Tags, tag) | ||||
| 				} | ||||
| 		rows := database.Query(query) | ||||
|  | ||||
| 		for rows.Next() { | ||||
| 			var tag types.Tag | ||||
| 			err := rows.Scan(&tag.TagName, &tag.TagId) | ||||
| 			if err != nil { | ||||
| 				panic(err.Error()) // proper error handling instead of panic in your app | ||||
| 			} | ||||
| 		} else { | ||||
| 			result.Tags = []types.Tag{} | ||||
| 			// append to final array | ||||
| 			result.Tags = append(result.Tags, tag) | ||||
| 		} | ||||
|  | ||||
| 		context.Json(result) | ||||
| 		// jsonify results | ||||
| 		str, _ := json.Marshal(result) | ||||
| 		return str | ||||
| 	}) | ||||
|  | ||||
| 	/** | ||||
| 	 * @api {post} /api/video [getSearchKeyWord] | ||||
| 	 * @apiDescription Get videos for search keyword | ||||
| 	 * @apiName getSearchKeyWord | ||||
| 	 * @apiGroup video | ||||
| 	 * | ||||
| 	 * @apiParam {string} KeyWord Keyword to search for | ||||
| 	 * | ||||
| 	 * @apiSuccess {Object[]} . List of Videos | ||||
| 	 * @apiSuccess {number} .MovieId Id of Video | ||||
| 	 * @apiSuccess {String} .MovieName  Name of video | ||||
| 	 */ | ||||
| 	api.AddHandler("getSearchKeyWord", api.VideoNode, api.PermUser, func(context api.Context) { | ||||
| 		var args struct { | ||||
| 			KeyWord string | ||||
| 		} | ||||
| 		if api.DecodeRequest(context.GetRequest(), &args) != nil { | ||||
| 			context.Text("unable to decode request") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 	var gsk struct { | ||||
| 		KeyWord string | ||||
| 	} | ||||
| 	AddHandler("getSearchKeyWord", VideoNode, &gsk, func() []byte { | ||||
| 		query := fmt.Sprintf(`SELECT movie_id,movie_name FROM videos  | ||||
| 					WHERE movie_name LIKE '%%%s%%' | ||||
| 					ORDER BY likes DESC, create_date DESC, movie_name`, args.KeyWord) | ||||
| 		context.Json(readVideosFromResultset(database.Query(query))) | ||||
| 					ORDER BY likes DESC, create_date DESC, movie_name`, gsk.KeyWord) | ||||
|  | ||||
| 		result := readVideosFromResultset(database.Query(query)) | ||||
| 		// jsonify results | ||||
| 		str, _ := json.Marshal(result) | ||||
| 		return str | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // function to handle stuff for loading specific videos and startdata | ||||
| func loadVideosHandlers() { | ||||
| 	/** | ||||
| 	 * @api {post} /api/video [loadVideo] | ||||
| 	 * @apiDescription Load all data for a specific video | ||||
| 	 * @apiName loadVideo | ||||
| 	 * @apiGroup video | ||||
| 	 * | ||||
| 	 * @apiParam {int} MovieId ID of video | ||||
| 	 * | ||||
| 	 * @apiSuccess {string} MovieName Videoname | ||||
| 	 * @apiSuccess {uint32} MovieId Video ID | ||||
| 	 * @apiSuccess {string} MovieUrl Url to video file | ||||
| 	 * @apiSuccess {string} Poster Base64 encoded Poster | ||||
| 	 * @apiSuccess {uint64} Likes Number of likes | ||||
| 	 * @apiSuccess {uint16} Quality Video FrameWidth | ||||
| 	 * @apiSuccess {uint16} Length Video Length in seconds | ||||
| 	 * | ||||
| 	 * | ||||
| 	 * @apiSuccess {Object[]} Tags Array of tags of video | ||||
| 	 * @apiSuccess {string} Tags.TagName Tagname | ||||
| 	 * @apiSuccess {uint32} Tags.TagId Tag ID | ||||
| 	 * | ||||
| 	 * @apiSuccess {Object[]} SuggestedTag Array of tags for quick add suggestions | ||||
| 	 * @apiSuccess {string} SuggestedTag.TagName Tagname | ||||
| 	 * @apiSuccess {uint32} SuggestedTag.TagId Tag ID | ||||
| 	 * | ||||
| 	 * @apiSuccess {Object[]} Actors Array of Actors playing in this video | ||||
| 	 * @apiSuccess {uint32} Actors.ActorId Actor Id | ||||
| 	 * @apiSuccess {string} Actors.Name Actor Name | ||||
| 	 * @apiSuccess {string} Actors.Thumbnail Portrait Thumbnail | ||||
| 	 */ | ||||
| 	api.AddHandler("loadVideo", api.VideoNode, api.PermUser, func(context api.Context) { | ||||
| 		var args struct { | ||||
| 			MovieId int | ||||
| 		} | ||||
| 		if api.DecodeRequest(context.GetRequest(), &args) != nil { | ||||
| 			context.Text("unable to decode request") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		query := fmt.Sprintf(`SELECT movie_name,movie_url,movie_id,thumbnail,poster,likes,quality,length,release_date  | ||||
| 										FROM videos WHERE movie_id=%d`, args.MovieId) | ||||
| 	var lv struct { | ||||
| 		MovieId int | ||||
| 	} | ||||
| 	AddHandler("loadVideo", VideoNode, &lv, func() []byte { | ||||
| 		query := fmt.Sprintf(`SELECT movie_name,movie_url,movie_id,thumbnail,poster,likes,quality,length  | ||||
| 										FROM videos WHERE movie_id=%d`, lv.MovieId) | ||||
|  | ||||
| 		var res types.FullVideoType | ||||
| 		var poster []byte | ||||
| 		var thumbnail []byte | ||||
|  | ||||
| 		err := database.QueryRow(query).Scan(&res.MovieName, &res.MovieUrl, &res.MovieId, &thumbnail, &poster, &res.Likes, &res.Quality, &res.Length, &res.ReleaseDate) | ||||
| 		err := database.QueryRow(query).Scan(&res.MovieName, &res.MovieUrl, &res.MovieId, &thumbnail, &poster, &res.Likes, &res.Quality, &res.Length) | ||||
| 		if err != nil { | ||||
| 			fmt.Printf("Error getting full data list of videoid - %d", args.MovieId) | ||||
| 			fmt.Printf("error getting full data list of videoid - %d", lv.MovieId) | ||||
| 			fmt.Println(err.Error()) | ||||
| 			return | ||||
| 			return nil | ||||
| 		} | ||||
|  | ||||
| 		// we ned to urlencode the movieurl | ||||
| @@ -326,7 +149,7 @@ func loadVideosHandlers() { | ||||
| 		query = fmt.Sprintf(`SELECT t.tag_id, t.tag_name FROM video_tags  | ||||
| 					INNER JOIN tags t on video_tags.tag_id = t.tag_id | ||||
| 					WHERE video_tags.video_id=%d | ||||
| 					GROUP BY t.tag_id`, args.MovieId) | ||||
| 					GROUP BY t.tag_id`, lv.MovieId) | ||||
|  | ||||
| 		res.Tags = readTagsFromResultset(database.Query(query)) | ||||
|  | ||||
| @@ -335,34 +158,23 @@ func loadVideosHandlers() { | ||||
| 						SELECT video_tags.tag_id FROM video_tags | ||||
| 					WHERE video_id=%d) | ||||
| 					ORDER BY rand() | ||||
| 					LIMIT 5`, args.MovieId) | ||||
| 					LIMIT 5`, lv.MovieId) | ||||
|  | ||||
| 		res.SuggestedTag = readTagsFromResultset(database.Query(query)) | ||||
|  | ||||
| 		// query the actors corresponding to video | ||||
| 		query = fmt.Sprintf(`SELECT a.actor_id, name, thumbnail FROM actors_videos | ||||
| 					JOIN actors a on actors_videos.actor_id = a.actor_id | ||||
| 					WHERE actors_videos.video_id=%d`, args.MovieId) | ||||
| 					WHERE actors_videos.video_id=%d`, lv.MovieId) | ||||
|  | ||||
| 		res.Actors = readActorsFromResultset(database.Query(query)) | ||||
|  | ||||
| 		context.Json(res) | ||||
| 		// jsonify results | ||||
| 		str, _ := json.Marshal(res) | ||||
| 		return str | ||||
| 	}) | ||||
|  | ||||
| 	/** | ||||
| 	 * @api {post} /api/video [getStartData] | ||||
| 	 * @apiDescription Get general video informations at start | ||||
| 	 * @apiName getStartData | ||||
| 	 * @apiGroup video | ||||
| 	 * | ||||
| 	 * @apiSuccess {uint32} VideoNr Total nr of videos | ||||
| 	 * @apiSuccess {uint32} FullHdNr number of FullHD videos | ||||
| 	 * @apiSuccess {uint32} HDNr number of HD videos | ||||
| 	 * @apiSuccess {uint32} SDNr number of SD videos | ||||
| 	 * @apiSuccess {uint32} DifferentTags number of different Tags available | ||||
| 	 * @apiSuccess {uint32} Tagged number of different Tags assigned | ||||
| 	 */ | ||||
| 	api.AddHandler("getStartData", api.VideoNode, api.PermUser, func(context api.Context) { | ||||
| 	AddHandler("getStartData", VideoNode, nil, func() []byte { | ||||
| 		var result types.StartData | ||||
| 		// query settings and infotile values | ||||
| 		query := ` | ||||
| @@ -396,90 +208,39 @@ func loadVideosHandlers() { | ||||
|  | ||||
| 		_ = database.QueryRow(query).Scan(&result.VideoNr, &result.Tagged, &result.HDNr, &result.FullHdNr, &result.SDNr, &result.DifferentTags) | ||||
|  | ||||
| 		context.Json(result) | ||||
| 		// jsonify results | ||||
| 		str, _ := json.Marshal(result) | ||||
| 		return str | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func addToVideoHandlers() { | ||||
| 	/** | ||||
| 	 * @api {post} /api/video [addLike] | ||||
| 	 * @apiDescription Add a like to a video | ||||
| 	 * @apiName addLike | ||||
| 	 * @apiGroup video | ||||
| 	 * | ||||
| 	 * @apiParam {int} MovieId ID of video | ||||
| 	 * | ||||
| 	 * @apiSuccess {string} result 'success' if successfully or Error message if not | ||||
| 	 */ | ||||
| 	api.AddHandler("addLike", api.VideoNode, api.PermUser, func(context api.Context) { | ||||
| 		var args struct { | ||||
| 			MovieId int | ||||
| 		} | ||||
| 		if api.DecodeRequest(context.GetRequest(), &args) != nil { | ||||
| 			context.Text("unable to decode request") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		query := fmt.Sprintf("update videos set likes = likes + 1 where movie_id = %d", args.MovieId) | ||||
| 		context.Text(string(database.SuccessQuery(query))) | ||||
| 	var al struct { | ||||
| 		MovieId int | ||||
| 	} | ||||
| 	AddHandler("addLike", VideoNode, &al, func() []byte { | ||||
| 		query := fmt.Sprintf("update videos set likes = likes + 1 where movie_id = %d", al.MovieId) | ||||
| 		return database.SuccessQuery(query) | ||||
| 	}) | ||||
|  | ||||
| 	/** | ||||
| 	 * @api {post} /api/video [deleteVideo] | ||||
| 	 * @apiDescription Delete a specific video from database | ||||
| 	 * @apiName deleteVideo | ||||
| 	 * @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 | ||||
| 	 */ | ||||
| 	api.AddHandler("deleteVideo", api.VideoNode, api.PermUser, func(context api.Context) { | ||||
| 		var args struct { | ||||
| 			MovieId     int | ||||
| 			FullyDelete bool | ||||
| 		} | ||||
| 		if api.DecodeRequest(context.GetRequest(), &args) != nil { | ||||
| 			context.Text("unable to decode request") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 	var dv struct { | ||||
| 		MovieId int | ||||
| 	} | ||||
| 	AddHandler("deleteVideo", VideoNode, &dv, func() []byte { | ||||
| 		// delete tag constraints | ||||
| 		query := fmt.Sprintf("DELETE FROM video_tags WHERE video_id=%d", args.MovieId) | ||||
| 		query := fmt.Sprintf("DELETE FROM video_tags WHERE video_id=%d", dv.MovieId) | ||||
| 		err := database.Edit(query) | ||||
|  | ||||
| 		// delete actor constraints | ||||
| 		query = fmt.Sprintf("DELETE FROM actors_videos WHERE video_id=%d", args.MovieId) | ||||
| 		query = fmt.Sprintf("DELETE FROM actors_videos WHERE video_id=%d", dv.MovieId) | ||||
| 		err = database.Edit(query) | ||||
|  | ||||
| 		// respond only if result not successful | ||||
| 		if err != nil { | ||||
| 			context.Text(string(database.ManualSuccessResponse(err))) | ||||
| 			return database.ManualSuccessResponse(err) | ||||
| 		} | ||||
|  | ||||
| 		// only allow deletion of video if cli flag is set, independent of passed api arg | ||||
| 		if config.GetConfig().Features.FullyDeletableVideos && 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 { | ||||
| 				context.Text(string(database.ManualSuccessResponse(err))) | ||||
| 			} | ||||
|  | ||||
| 			sett, videoprefix, _ := database.GetSettings() | ||||
| 			assembledPath := videoprefix + sett.VideoPath + vidpath | ||||
|  | ||||
| 			err = os.Remove(assembledPath) | ||||
| 			if err != nil { | ||||
| 				fmt.Printf("unable to delete file: %s -- %s\n", assembledPath, err.Error()) | ||||
| 				context.Text(string(database.ManualSuccessResponse(err))) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// delete video row from db | ||||
| 		query = fmt.Sprintf("DELETE FROM videos WHERE movie_id=%d", args.MovieId) | ||||
| 		context.Text(string(database.SuccessQuery(query))) | ||||
| 		query = fmt.Sprintf("DELETE FROM videos WHERE movie_id=%d", dv.MovieId) | ||||
| 		return database.SuccessQuery(query) | ||||
| 	}) | ||||
| } | ||||
|   | ||||
| @@ -1,64 +0,0 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"openmediacenter/apiGo/database/settings" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	VideoNode    = "video" | ||||
| 	TagNode      = "tags" | ||||
| 	SettingsNode = "settings" | ||||
| 	ActorNode    = "actor" | ||||
| 	TVShowNode   = "tv" | ||||
| 	LoginNode    = "login" | ||||
| ) | ||||
|  | ||||
| func AddHandler(action string, apiNode string, perm Perm, handler func(ctx Context)) { | ||||
| 	http.Handle(fmt.Sprintf("/api/%s/%s", apiNode, action), http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { | ||||
| 		srvPwd := settings.GetPassword() | ||||
| 		if srvPwd == nil { | ||||
| 			// no password set | ||||
| 			ctx := &apicontext{writer: writer, responseWritten: false, request: request, userid: -1, permid: PermUnauthorized} | ||||
| 			callHandler(ctx, handler, writer) | ||||
| 		} else { | ||||
| 			tokenheader := request.Header.Get("Token") | ||||
|  | ||||
| 			id := -1 | ||||
| 			permid := PermUnauthorized | ||||
|  | ||||
| 			// check token if token provided | ||||
| 			if tokenheader != "" { | ||||
| 				id, permid = TokenValid(request.Header.Get("Token")) | ||||
| 			} | ||||
|  | ||||
| 			ctx := &apicontext{writer: writer, responseWritten: false, request: request, userid: id, permid: permid} | ||||
|  | ||||
| 			// check if rights are sufficient to perform the action | ||||
| 			if permid <= perm { | ||||
| 				callHandler(ctx, handler, writer) | ||||
| 			} else { | ||||
| 				ctx.Error("insufficient permissions") | ||||
| 			} | ||||
| 		} | ||||
| 	})) | ||||
| } | ||||
|  | ||||
| func callHandler(ctx *apicontext, handler func(ctx Context), writer http.ResponseWriter) { | ||||
| 	handler(ctx) | ||||
|  | ||||
| 	if !ctx.responseWritten { | ||||
| 		// none of the response functions called so send default response | ||||
| 		ctx.Error("Unknown server Error occured") | ||||
| 		writer.WriteHeader(501) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func ServerInit(port uint16) error { | ||||
| 	// initialize auth service and add corresponding auth routes | ||||
| 	InitOAuth() | ||||
|  | ||||
| 	fmt.Printf("OpenMediacenter server up and running on port %d\n", port) | ||||
| 	return http.ListenAndServe(fmt.Sprintf(":%d", port), nil) | ||||
| } | ||||
| @@ -1,100 +0,0 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"github.com/dgrijalva/jwt-go" | ||||
| 	"openmediacenter/apiGo/database" | ||||
| 	"strconv" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type Perm uint8 | ||||
|  | ||||
| const ( | ||||
| 	PermAdmin Perm = iota | ||||
| 	PermUser | ||||
| 	PermUnauthorized | ||||
| ) | ||||
|  | ||||
| func (p Perm) String() string { | ||||
| 	return [...]string{"PermAdmin", "PermUser", "PermUnauthorized"}[p] | ||||
| } | ||||
|  | ||||
| const SignKey = "89013f1753a6890c6090b09e3c23ff43" | ||||
| const TokenExpireHours = 8760 | ||||
|  | ||||
| type Token struct { | ||||
| 	Token     string | ||||
| 	ExpiresAt int64 | ||||
| } | ||||
|  | ||||
| func TokenValid(token string) (int, Perm) { | ||||
| 	t, err := jwt.ParseWithClaims(token, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) { | ||||
| 		return []byte(SignKey), nil | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return -1, PermUnauthorized | ||||
| 	} | ||||
|  | ||||
| 	claims := t.Claims.(*jwt.StandardClaims) | ||||
|  | ||||
| 	id, err := strconv.Atoi(claims.Issuer) | ||||
| 	permid, err := strconv.Atoi(claims.Subject) | ||||
| 	if err != nil { | ||||
| 		return -1, PermUnauthorized | ||||
| 	} | ||||
| 	return id, Perm(permid) | ||||
| } | ||||
|  | ||||
| func InitOAuth() { | ||||
| 	AddHandler("login", LoginNode, PermUnauthorized, func(ctx Context) { | ||||
| 		var t struct { | ||||
| 			Password string | ||||
| 		} | ||||
|  | ||||
| 		if DecodeRequest(ctx.GetRequest(), &t) != nil { | ||||
| 			fmt.Println("Error accured while decoding Testrequest!!") | ||||
| 		} | ||||
|  | ||||
| 		// empty check | ||||
| 		if t.Password == "" { | ||||
| 			ctx.Error("empty password") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		// generate Argon2 Hash of passed pwd | ||||
| 		HashPassword(t.Password) | ||||
| 		// todo use hashed password | ||||
|  | ||||
| 		var password string | ||||
|  | ||||
| 		err := database.QueryRow("SELECT password FROM settings WHERE 1").Scan(&password) | ||||
| 		if err != nil || t.Password != password { | ||||
| 			ctx.Error("unauthorized") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		expires := time.Now().Add(time.Hour * TokenExpireHours).Unix() | ||||
| 		claims := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.StandardClaims{ | ||||
| 			Issuer:    strconv.Itoa(int(0)), | ||||
| 			Subject:   strconv.Itoa(int(PermUser)), | ||||
| 			ExpiresAt: expires, | ||||
| 		}) | ||||
|  | ||||
| 		token, err := claims.SignedString([]byte(SignKey)) | ||||
| 		if err != nil { | ||||
| 			fmt.Println(err.Error()) | ||||
| 			ctx.Error("failed to generate authorization token") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		type ResponseType struct { | ||||
| 			Token Token | ||||
| 		} | ||||
|  | ||||
| 		ctx.Json(Token{ | ||||
| 			Token:     token, | ||||
| 			ExpiresAt: expires, | ||||
| 		}) | ||||
| 	}) | ||||
| } | ||||
| @@ -1,65 +0,0 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| ) | ||||
|  | ||||
| type Context interface { | ||||
| 	Json(t interface{}) | ||||
| 	Text(msg string) | ||||
| 	Error(msg string) | ||||
| 	Errorf(msg string, args ...interface{}) | ||||
| 	GetRequest() *http.Request | ||||
| 	GetWriter() http.ResponseWriter | ||||
| 	UserID() int | ||||
| 	PermID() Perm | ||||
| } | ||||
|  | ||||
| type apicontext struct { | ||||
| 	writer          http.ResponseWriter | ||||
| 	request         *http.Request | ||||
| 	responseWritten bool | ||||
| 	userid          int | ||||
| 	permid          Perm | ||||
| } | ||||
|  | ||||
| func (r *apicontext) GetRequest() *http.Request { | ||||
| 	return r.request | ||||
| } | ||||
|  | ||||
| func (r *apicontext) UserID() int { | ||||
| 	return r.userid | ||||
| } | ||||
|  | ||||
| func (r *apicontext) GetWriter() http.ResponseWriter { | ||||
| 	return r.writer | ||||
| } | ||||
|  | ||||
| func (r *apicontext) Json(t interface{}) { | ||||
| 	r.writer.Write(Jsonify(t)) | ||||
| 	r.responseWritten = true | ||||
| } | ||||
|  | ||||
| func (r *apicontext) Text(msg string) { | ||||
| 	r.writer.Write([]byte(msg)) | ||||
| 	r.responseWritten = true | ||||
| } | ||||
|  | ||||
| func (r *apicontext) Error(msg string) { | ||||
| 	type Error struct { | ||||
| 		Message string | ||||
| 	} | ||||
|  | ||||
| 	r.writer.WriteHeader(500) | ||||
| 	r.writer.Write(Jsonify(Error{Message: msg})) | ||||
| 	r.responseWritten = true | ||||
| } | ||||
|  | ||||
| func (r *apicontext) Errorf(msg string, args ...interface{}) { | ||||
| 	r.Error(fmt.Sprintf(msg, &args)) | ||||
| } | ||||
|  | ||||
| func (r *apicontext) PermID() Perm { | ||||
| 	return r.permid | ||||
| } | ||||
| @@ -1,13 +0,0 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"encoding/hex" | ||||
| 	"golang.org/x/crypto/argon2" | ||||
| ) | ||||
|  | ||||
| func HashPassword(pwd string) *string { | ||||
| 	// todo generate random salt | ||||
| 	hash := argon2.IDKey([]byte(pwd), []byte(SignKey), 3, 64*1024, 2, 32) | ||||
| 	hexx := hex.EncodeToString(hash) | ||||
| 	return &hexx | ||||
| } | ||||
| @@ -1,11 +0,0 @@ | ||||
| package api | ||||
|  | ||||
| import "testing" | ||||
|  | ||||
| func TestHashlength(t *testing.T) { | ||||
| 	h := HashPassword("test") | ||||
|  | ||||
| 	if len(*h) != 64 { | ||||
| 		t.Errorf("Invalid hash length: %d", len(*h)) | ||||
| 	} | ||||
| } | ||||
| @@ -1,31 +0,0 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| ) | ||||
|  | ||||
| func Jsonify(v interface{}) []byte { | ||||
| 	// Jsonify results | ||||
| 	str, err := json.Marshal(v) | ||||
| 	if err != nil { | ||||
| 		fmt.Println("Error while Jsonifying return object: " + err.Error()) | ||||
| 	} | ||||
| 	return str | ||||
| } | ||||
|  | ||||
| // DecodeRequest decodes the request | ||||
| func DecodeRequest(request *http.Request, arg interface{}) error { | ||||
| 	buf := new(bytes.Buffer) | ||||
| 	buf.ReadFrom(request.Body) | ||||
| 	body := buf.String() | ||||
|  | ||||
| 	err := json.Unmarshal([]byte(body), &arg) | ||||
| 	if err != nil { | ||||
| 		fmt.Println("JSON decode Error" + err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	return err | ||||
| } | ||||
| @@ -1,22 +0,0 @@ | ||||
| package api | ||||
|  | ||||
| import "testing" | ||||
|  | ||||
| func TestJsonify(t *testing.T) { | ||||
| 	var obj = struct { | ||||
| 		ID  uint32 | ||||
| 		Str string | ||||
| 		Boo bool | ||||
| 	}{ | ||||
| 		ID:  42, | ||||
| 		Str: "teststr", | ||||
| 		Boo: true, | ||||
| 	} | ||||
|  | ||||
| 	res := Jsonify(obj) | ||||
| 	exp := `{"ID":42,"Str":"teststr","Boo":true}` | ||||
|  | ||||
| 	if string(res) != exp { | ||||
| 		t.Errorf("Invalid json response: %s !== %s", string(res), exp) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										57
									
								
								apiGo/api/oauth/CustomClientStore.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								apiGo/api/oauth/CustomClientStore.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| package oauth | ||||
|  | ||||
| import ( | ||||
| 	"gopkg.in/oauth2.v3" | ||||
| 	"openmediacenter/apiGo/database/settings" | ||||
| ) | ||||
|  | ||||
| type CustomClientStore struct { | ||||
| 	oauth2.ClientStore | ||||
| } | ||||
|  | ||||
| type CustomClientInfo struct { | ||||
| 	oauth2.ClientInfo | ||||
| 	ID     string | ||||
| 	Secret string | ||||
| 	Domain string | ||||
| 	UserID string | ||||
| } | ||||
|  | ||||
| func NewCustomStore() oauth2.ClientStore { | ||||
| 	s := new(CustomClientStore) | ||||
| 	return s | ||||
| } | ||||
|  | ||||
| func (a *CustomClientStore) GetByID(id string) (oauth2.ClientInfo, error) { | ||||
| 	password := settings.GetPassword() | ||||
| 	// if password not set assign default password | ||||
| 	if password == nil { | ||||
| 		defaultpassword := "openmediacenter" | ||||
| 		password = &defaultpassword | ||||
| 	} | ||||
|  | ||||
| 	clientinfo := CustomClientInfo{ | ||||
| 		ID:     "openmediacenter", | ||||
| 		Secret: *password, | ||||
| 		Domain: "http://localhost:8081", | ||||
| 		UserID: "openmediacenter", | ||||
| 	} | ||||
|  | ||||
| 	return &clientinfo, nil | ||||
| } | ||||
|  | ||||
| func (a *CustomClientInfo) GetID() string { | ||||
| 	return a.ID | ||||
| } | ||||
|  | ||||
| func (a *CustomClientInfo) GetSecret() string { | ||||
| 	return a.Secret | ||||
| } | ||||
|  | ||||
| func (a *CustomClientInfo) GetDomain() string { | ||||
| 	return a.Domain | ||||
| } | ||||
|  | ||||
| func (a *CustomClientInfo) GetUserID() string { | ||||
| 	return a.UserID | ||||
| } | ||||
							
								
								
									
										61
									
								
								apiGo/api/oauth/Oauth.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								apiGo/api/oauth/Oauth.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| package oauth | ||||
|  | ||||
| import ( | ||||
| 	"gopkg.in/oauth2.v3/errors" | ||||
| 	"gopkg.in/oauth2.v3/manage" | ||||
| 	"gopkg.in/oauth2.v3/server" | ||||
| 	"gopkg.in/oauth2.v3/store" | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| ) | ||||
|  | ||||
| var srv *server.Server | ||||
|  | ||||
| func InitOAuth() { | ||||
| 	manager := manage.NewDefaultManager() | ||||
| 	// token store | ||||
| 	manager.MustTokenStorage(store.NewMemoryTokenStore()) | ||||
|  | ||||
| 	// create new secretstore | ||||
| 	clientStore := NewCustomStore() | ||||
| 	manager.MapClientStorage(clientStore) | ||||
|  | ||||
| 	srv = server.NewServer(server.NewConfig(), manager) | ||||
| 	srv.SetClientInfoHandler(server.ClientFormHandler) | ||||
| 	manager.SetRefreshTokenCfg(manage.DefaultRefreshTokenCfg) | ||||
|  | ||||
| 	srv.SetInternalErrorHandler(func(err error) (re *errors.Response) { | ||||
| 		log.Println("Internal Error:", err.Error()) | ||||
| 		return | ||||
| 	}) | ||||
|  | ||||
| 	srv.SetResponseErrorHandler(func(re *errors.Response) { | ||||
| 		log.Println("Response Error:", re.Error.Error()) | ||||
| 	}) | ||||
|  | ||||
| 	http.HandleFunc("/authorize", func(w http.ResponseWriter, r *http.Request) { | ||||
| 		err := srv.HandleAuthorizeRequest(w, r) | ||||
| 		if err != nil { | ||||
| 			http.Error(w, err.Error(), http.StatusBadRequest) | ||||
| 		} | ||||
| 	}) | ||||
|  | ||||
| 	http.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) { | ||||
| 		err := srv.HandleTokenRequest(w, r) | ||||
| 		if err != nil { | ||||
| 			http.Error(w, err.Error(), http.StatusInternalServerError) | ||||
| 		} | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func ValidateToken(f http.HandlerFunc) http.HandlerFunc { | ||||
| 	return func(w http.ResponseWriter, r *http.Request) { | ||||
| 		_, err := srv.ValidationBearerToken(r) | ||||
| 		if err != nil { | ||||
| 			http.Error(w, err.Error(), http.StatusBadRequest) | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		f.ServeHTTP(w, r) | ||||
| 	} | ||||
| } | ||||
| @@ -3,18 +3,16 @@ package types | ||||
| type VideoUnloadedType struct { | ||||
| 	MovieId   int | ||||
| 	MovieName string | ||||
| 	Ratio     float32 | ||||
| } | ||||
|  | ||||
| type FullVideoType struct { | ||||
| 	MovieName    string | ||||
| 	MovieId      uint32 | ||||
| 	MovieId      int | ||||
| 	MovieUrl     string | ||||
| 	Poster       string | ||||
| 	ReleaseDate  *string | ||||
| 	Likes        uint64 | ||||
| 	Quality      uint16 | ||||
| 	Length       uint16 | ||||
| 	Likes        int | ||||
| 	Quality      int | ||||
| 	Length       int | ||||
| 	Tags         []Tag | ||||
| 	SuggestedTag []Tag | ||||
| 	Actors       []Actor | ||||
| @@ -22,22 +20,22 @@ type FullVideoType struct { | ||||
|  | ||||
| type Tag struct { | ||||
| 	TagName string | ||||
| 	TagId   uint32 | ||||
| 	TagId   int | ||||
| } | ||||
|  | ||||
| type Actor struct { | ||||
| 	ActorId   uint32 | ||||
| 	ActorId   int | ||||
| 	Name      string | ||||
| 	Thumbnail string | ||||
| } | ||||
|  | ||||
| type StartData struct { | ||||
| 	VideoNr       uint32 | ||||
| 	FullHdNr      uint32 | ||||
| 	HDNr          uint32 | ||||
| 	SDNr          uint32 | ||||
| 	DifferentTags uint32 | ||||
| 	Tagged        uint32 | ||||
| 	VideoNr       int | ||||
| 	FullHdNr      int | ||||
| 	HDNr          int | ||||
| 	SDNr          int | ||||
| 	DifferentTags int | ||||
| 	Tagged        int | ||||
| } | ||||
|  | ||||
| type SettingsType struct { | ||||
| @@ -48,16 +46,11 @@ type SettingsType struct { | ||||
| 	PasswordEnabled bool | ||||
| 	TMDBGrabbing    bool | ||||
| 	DarkMode        bool | ||||
| } | ||||
|  | ||||
| type SettingsSizeType struct { | ||||
| 	VideoNr       uint32 | ||||
| 	VideoNr       int | ||||
| 	DBSize        float32 | ||||
| 	DifferentTags uint32 | ||||
| 	TagsAdded     uint32 | ||||
| } | ||||
| 	DifferentTags int | ||||
| 	TagsAdded     int | ||||
|  | ||||
| type TVShow struct { | ||||
| 	Id   uint32 | ||||
| 	Name string | ||||
| 	PathPrefix string | ||||
| } | ||||
|   | ||||
| @@ -1,206 +0,0 @@ | ||||
| package config | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"flag" | ||||
| 	"fmt" | ||||
| 	"github.com/pelletier/go-toml/v2" | ||||
| 	"os" | ||||
| ) | ||||
|  | ||||
| type DatabaseT struct { | ||||
| 	DBName     string | ||||
| 	DBPassword string | ||||
| 	DBUser     string | ||||
| 	DBPort     uint16 | ||||
| 	DBHost     string | ||||
| } | ||||
|  | ||||
| type FeaturesT struct { | ||||
| 	DisableTVSupport     bool | ||||
| 	FullyDeletableVideos bool | ||||
| } | ||||
|  | ||||
| type GeneralT struct { | ||||
| 	VerboseLogging bool | ||||
| 	ReindexPrefix  string | ||||
| } | ||||
|  | ||||
| type FileConfT struct { | ||||
| 	Database DatabaseT | ||||
| 	General  GeneralT | ||||
| 	Features FeaturesT | ||||
| } | ||||
|  | ||||
| func defaultConfig() *FileConfT { | ||||
| 	return &FileConfT{ | ||||
| 		Database: DatabaseT{ | ||||
| 			DBName:     "mediacenter", | ||||
| 			DBPassword: "mediapassword", | ||||
| 			DBUser:     "mediacenteruser", | ||||
| 			DBPort:     3306, | ||||
| 			DBHost:     "127.0.0.1", | ||||
| 		}, | ||||
| 		General: GeneralT{ | ||||
| 			VerboseLogging: false, | ||||
| 			ReindexPrefix:  "/var/www/openmediacenter", | ||||
| 		}, | ||||
| 		Features: FeaturesT{ | ||||
| 			DisableTVSupport:     false, | ||||
| 			FullyDeletableVideos: false, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| var liveConf FileConfT | ||||
|  | ||||
| func Init() { | ||||
| 	cfgname := "openmediacenter.cfg" | ||||
| 	cfgpath := "/etc/" | ||||
|  | ||||
| 	// load config from disk | ||||
| 	dat, err := os.ReadFile(cfgpath + cfgname) | ||||
| 	if err != nil { | ||||
| 		// handle error if not exists or no sufficient read access | ||||
| 		if _, ok := err.(*os.PathError); ok { | ||||
|  | ||||
| 			// check if config exists on local dir | ||||
| 			dat, err = os.ReadFile(cfgname) | ||||
| 			if err != nil { | ||||
| 				generateNewConfig(cfgpath, cfgname) | ||||
| 			} else { | ||||
| 				// ok decode local config | ||||
| 				decodeConfig(&dat) | ||||
| 			} | ||||
| 		} else { | ||||
| 			// other error | ||||
| 			fmt.Println(err.Error()) | ||||
| 		} | ||||
| 	} else { | ||||
| 		decodeConfig(&dat) | ||||
| 	} | ||||
|  | ||||
| 	handleCommandLineArguments() | ||||
| } | ||||
|  | ||||
| func generateNewConfig(cfgpath string, cfgname string) { | ||||
| 	// config really doesn't exist! | ||||
| 	fmt.Printf("config not existing -- generating new empty config at %s%s\n", cfgpath, cfgname) | ||||
|  | ||||
| 	// generate new default config | ||||
| 	obj, _ := toml.Marshal(defaultConfig()) | ||||
| 	liveConf = *defaultConfig() | ||||
|  | ||||
| 	err := os.WriteFile(cfgpath+cfgname, obj, 777) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, os.ErrPermission) { | ||||
| 			// permisson denied to create file try to create at current dir | ||||
| 			err = os.WriteFile(cfgname, obj, 777) | ||||
| 			if err != nil { | ||||
| 				fmt.Println("failed to create default config file!") | ||||
| 			} else { | ||||
| 				fmt.Println("config file created at .") | ||||
| 			} | ||||
| 		} else { | ||||
| 			fmt.Println(err.Error()) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func decodeConfig(bin *[]byte) { | ||||
| 	config := FileConfT{} | ||||
|  | ||||
| 	err := toml.Unmarshal(*bin, &config) | ||||
| 	if err != nil { | ||||
| 		fmt.Println(err) | ||||
| 		liveConf = *defaultConfig() | ||||
| 	} else { | ||||
| 		fmt.Println("Successfully loaded config file!") | ||||
|  | ||||
| 		liveConf = config | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // handleCommandLineArguments let cli args override the config | ||||
| func handleCommandLineArguments() { | ||||
| 	// get a defaultconfig obj to set defaults | ||||
| 	dconf := defaultConfig() | ||||
|  | ||||
| 	const ( | ||||
| 		DBHost               = "DBHost" | ||||
| 		DBPort               = "DBPort" | ||||
| 		DBUser               = "DBUser" | ||||
| 		DBPassword           = "DBPassword" | ||||
| 		DBName               = "DBName" | ||||
| 		Verbose              = "v" | ||||
| 		ReindexPrefix        = "ReindexPrefix" | ||||
| 		DisableTVSupport     = "DisableTVSupport" | ||||
| 		FullyDeletableVideos = "FullyDeletableVideos" | ||||
| 	) | ||||
|  | ||||
| 	dbhostPtr := flag.String(DBHost, dconf.Database.DBHost, "database host name") | ||||
| 	dbPortPtr := flag.Int(DBPort, int(dconf.Database.DBPort), "database port") | ||||
| 	dbUserPtr := flag.String(DBUser, dconf.Database.DBUser, "database username") | ||||
| 	dbPassPtr := flag.String(DBPassword, dconf.Database.DBPassword, "database username") | ||||
| 	dbNamePtr := flag.String(DBName, dconf.Database.DBName, "database name") | ||||
|  | ||||
| 	verbosePtr := flag.Bool(Verbose, dconf.General.VerboseLogging, "Verbose log output") | ||||
|  | ||||
| 	pathPrefix := flag.String(ReindexPrefix, dconf.General.ReindexPrefix, "Prefix path for videos to reindex") | ||||
|  | ||||
| 	disableTVShowSupport := flag.Bool(DisableTVSupport, dconf.Features.DisableTVSupport, "Disable the TVShow support and pages") | ||||
| 	videosFullyDeletable := flag.Bool(FullyDeletableVideos, dconf.Features.FullyDeletableVideos, "Allow deletion from harddisk") | ||||
|  | ||||
| 	flag.Parse() | ||||
|  | ||||
| 	if isFlagPassed(DBHost) { | ||||
| 		liveConf.Database.DBHost = *dbhostPtr | ||||
| 	} | ||||
|  | ||||
| 	if isFlagPassed(DBPort) { | ||||
| 		liveConf.Database.DBPort = uint16(*dbPortPtr) | ||||
| 	} | ||||
|  | ||||
| 	if isFlagPassed(DBName) { | ||||
| 		liveConf.Database.DBName = *dbNamePtr | ||||
| 	} | ||||
|  | ||||
| 	if isFlagPassed(DBUser) { | ||||
| 		liveConf.Database.DBUser = *dbUserPtr | ||||
| 	} | ||||
|  | ||||
| 	if isFlagPassed(DBPassword) { | ||||
| 		liveConf.Database.DBPassword = *dbPassPtr | ||||
| 	} | ||||
|  | ||||
| 	if isFlagPassed(Verbose) { | ||||
| 		liveConf.General.VerboseLogging = *verbosePtr | ||||
| 	} | ||||
|  | ||||
| 	if isFlagPassed(ReindexPrefix) { | ||||
| 		liveConf.General.ReindexPrefix = *pathPrefix | ||||
| 	} | ||||
|  | ||||
| 	if isFlagPassed(DisableTVSupport) { | ||||
| 		liveConf.Features.DisableTVSupport = *disableTVShowSupport | ||||
| 	} | ||||
|  | ||||
| 	if isFlagPassed(FullyDeletableVideos) { | ||||
| 		liveConf.Features.FullyDeletableVideos = *videosFullyDeletable | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // isFlagPassed check whether a flag was passed | ||||
| func isFlagPassed(name string) bool { | ||||
| 	found := false | ||||
| 	flag.Visit(func(f *flag.Flag) { | ||||
| 		if f.Name == name { | ||||
| 			found = true | ||||
| 		} | ||||
| 	}) | ||||
| 	return found | ||||
| } | ||||
|  | ||||
| func GetConfig() *FileConfT { | ||||
| 	return &liveConf | ||||
| } | ||||
| @@ -1,9 +0,0 @@ | ||||
| package config | ||||
|  | ||||
| import "testing" | ||||
|  | ||||
| func TestSaveLoadConfig(t *testing.T) { | ||||
| 	generateNewConfig("", "openmediacenter.cfg") | ||||
|  | ||||
| 	Init() | ||||
| } | ||||
| @@ -2,24 +2,26 @@ package database | ||||
|  | ||||
| import ( | ||||
| 	"database/sql" | ||||
| 	"embed" | ||||
| 	"fmt" | ||||
| 	_ "github.com/go-sql-driver/mysql" | ||||
| 	"github.com/pressly/goose/v3" | ||||
| 	"log" | ||||
| 	"openmediacenter/apiGo/api/types" | ||||
| 	"openmediacenter/apiGo/config" | ||||
| 	"os" | ||||
| ) | ||||
|  | ||||
| var db *sql.DB | ||||
| var DBName string | ||||
|  | ||||
| //go:embed migrations/*.sql | ||||
| var embedMigrations embed.FS | ||||
| // store the command line parameter for Videoprefix | ||||
| var SettingsVideoPrefix = "" | ||||
|  | ||||
| func InitDB() error { | ||||
| 	dbconf := config.GetConfig().Database | ||||
| type DatabaseConfig struct { | ||||
| 	DBHost     string | ||||
| 	DBPort     int | ||||
| 	DBUser     string | ||||
| 	DBPassword string | ||||
| 	DBName     string | ||||
| } | ||||
|  | ||||
| func InitDB(dbconf *DatabaseConfig) { | ||||
| 	DBName = dbconf.DBName | ||||
|  | ||||
| 	// Open up our database connection. | ||||
| @@ -28,32 +30,15 @@ func InitDB() error { | ||||
|  | ||||
| 	// if there is an error opening the connection, handle it | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("Error while connecting to database! - %s\n", err.Error()) | ||||
| 		fmt.Printf("Error while connecting to database! - %s\n", err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	if db != nil { | ||||
| 		ping := db.Ping() | ||||
| 		if ping != nil { | ||||
| 			return fmt.Errorf("Error while connecting to database! - %s\n", ping.Error()) | ||||
| 			fmt.Printf("Error while connecting to database! - %s\n", ping.Error()) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	fmt.Println("Running Database migrations!") | ||||
| 	// perform database migrations | ||||
| 	goose.SetBaseFS(embedMigrations) | ||||
| 	goose.SetLogger(log.New(os.Stdout, "", 0)) | ||||
|  | ||||
| 	// set mysql dialect | ||||
| 	err = goose.SetDialect("mysql") | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if err := goose.Up(db, "migrations"); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func Query(query string, args ...interface{}) *sql.Rows { | ||||
| @@ -105,7 +90,9 @@ func Close() { | ||||
| 	db.Close() | ||||
| } | ||||
|  | ||||
| func GetSettings() (result types.SettingsType, PathPrefix string, sizes types.SettingsSizeType) { | ||||
| func GetSettings() types.SettingsType { | ||||
| 	var result types.SettingsType | ||||
|  | ||||
| 	// query settings and infotile values | ||||
| 	query := fmt.Sprintf(` | ||||
|                 SELECT ( | ||||
| @@ -133,7 +120,7 @@ func GetSettings() (result types.SettingsType, PathPrefix string, sizes types.Se | ||||
| 	var DarkMode int | ||||
| 	var TMDBGrabbing int | ||||
|  | ||||
| 	err := QueryRow(query).Scan(&sizes.VideoNr, &sizes.DBSize, &sizes.DifferentTags, &sizes.TagsAdded, | ||||
| 	err := QueryRow(query).Scan(&result.VideoNr, &result.DBSize, &result.DifferentTags, &result.TagsAdded, | ||||
| 		&result.VideoPath, &result.EpisodePath, &result.Password, &result.MediacenterName, &TMDBGrabbing, &DarkMode) | ||||
|  | ||||
| 	if err != nil { | ||||
| @@ -143,6 +130,7 @@ func GetSettings() (result types.SettingsType, PathPrefix string, sizes types.Se | ||||
| 	result.TMDBGrabbing = TMDBGrabbing != 0 | ||||
| 	result.PasswordEnabled = result.Password != "-1" | ||||
| 	result.DarkMode = DarkMode != 0 | ||||
| 	PathPrefix = config.GetConfig().General.ReindexPrefix | ||||
| 	return | ||||
| 	result.PathPrefix = SettingsVideoPrefix | ||||
|  | ||||
| 	return result | ||||
| } | ||||
|   | ||||
| @@ -1,122 +0,0 @@ | ||||
| -- +goose Up | ||||
| -- +goose StatementBegin | ||||
| create table if not exists actors | ||||
| ( | ||||
|     actor_id  int auto_increment | ||||
|         primary key, | ||||
|     name      varchar(50) null, | ||||
|     thumbnail mediumblob  null | ||||
| ) | ||||
|     comment 'informations about different actors'; | ||||
| -- +goose StatementEnd | ||||
| -- +goose StatementBegin | ||||
| create table if not exists settings | ||||
| ( | ||||
|     video_path       varchar(255)                          null, | ||||
|     episode_path     varchar(255)                          null, | ||||
|     password         varchar(32) default '-1'              null, | ||||
|     mediacenter_name varchar(32) default 'OpenMediaCenter' null, | ||||
|     TMDB_grabbing    tinyint                               null, | ||||
|     DarkMode         tinyint     default 0                 null | ||||
| ); | ||||
| -- +goose StatementEnd | ||||
| -- +goose StatementBegin | ||||
| create table if not exists tags | ||||
| ( | ||||
|     tag_id   int auto_increment | ||||
|         primary key, | ||||
|     tag_name varchar(50) null | ||||
| ); | ||||
| -- +goose StatementEnd | ||||
| -- +goose StatementBegin | ||||
| create table if not exists tvshow | ||||
| ( | ||||
|     name       varchar(100) null, | ||||
|     thumbnail  mediumblob   null, | ||||
|     id         int auto_increment | ||||
|         primary key, | ||||
|     foldername varchar(100) null | ||||
| ); | ||||
| -- +goose StatementEnd | ||||
| -- +goose StatementBegin | ||||
| create table if not exists tvshow_episodes | ||||
| ( | ||||
|     id        int auto_increment | ||||
|         primary key, | ||||
|     name      varchar(100) null, | ||||
|     season    int          null, | ||||
|     poster    mediumblob   null, | ||||
|     tvshow_id int          null, | ||||
|     episode   int          null, | ||||
|     filename  varchar(100) null, | ||||
|     constraint tvshow_episodes_tvshow_id_fk | ||||
|         foreign key (tvshow_id) references tvshow (id) | ||||
| ); | ||||
| -- +goose StatementEnd | ||||
| -- +goose StatementBegin | ||||
| create table if not exists videos | ||||
| ( | ||||
|     movie_id    int auto_increment | ||||
|         primary key, | ||||
|     movie_name  varchar(200)                         null, | ||||
|     movie_url   varchar(250)                         null, | ||||
|     thumbnail   mediumblob                           null, | ||||
|     poster      mediumblob                           null, | ||||
|     likes       int      default 0                   null, | ||||
|     quality     int      default 0                   null, | ||||
|     length      int      default 0                   null comment 'in seconds', | ||||
|     create_date datetime default current_timestamp() null | ||||
| ); | ||||
| -- +goose StatementEnd | ||||
| -- +goose StatementBegin | ||||
| create table if not exists actors_videos | ||||
| ( | ||||
|     actor_id int null, | ||||
|     video_id int null, | ||||
|     constraint actors_videos_actors_id_fk | ||||
|         foreign key (actor_id) references actors (actor_id), | ||||
|     constraint actors_videos_videos_movie_id_fk | ||||
|         foreign key (video_id) references videos (movie_id) | ||||
| ); | ||||
| -- +goose StatementEnd | ||||
| -- +goose StatementBegin | ||||
| create index if not exists actors_videos_actor_id_index | ||||
|     on actors_videos (actor_id); | ||||
| -- +goose StatementEnd | ||||
| -- +goose StatementBegin | ||||
| create index if not exists actors_videos_video_id_index | ||||
|     on actors_videos (video_id); | ||||
| -- +goose StatementEnd | ||||
| -- +goose StatementBegin | ||||
| create table if not exists video_tags | ||||
| ( | ||||
|     tag_id   int null, | ||||
|     video_id int null, | ||||
|     constraint video_tags_tags_tag_id_fk | ||||
|         foreign key (tag_id) references tags (tag_id), | ||||
|     constraint video_tags_videos_movie_id_fk | ||||
|         foreign key (video_id) references videos (movie_id) | ||||
|             on delete cascade | ||||
| ); | ||||
|  | ||||
| -- +goose StatementEnd | ||||
| -- +goose StatementBegin | ||||
| INSERT IGNORE INTO tags (tag_id, tag_name) | ||||
| VALUES (2, 'fullhd'); | ||||
| -- +goose StatementEnd | ||||
| -- +goose StatementBegin | ||||
| INSERT IGNORE INTO tags (tag_id, tag_name) | ||||
| VALUES (3, 'lowquality'); | ||||
| -- +goose StatementEnd | ||||
| -- +goose StatementBegin | ||||
| INSERT IGNORE INTO tags (tag_id, tag_name) | ||||
| VALUES (4, 'hd'); | ||||
| -- +goose StatementEnd | ||||
| -- +goose StatementBegin | ||||
| INSERT IGNORE INTO settings (video_path, episode_path, password, mediacenter_name) | ||||
| VALUES ('./videos/', './tvshows/', -1, 'OpenMediaCenter'); | ||||
|  | ||||
|  | ||||
| -- +goose StatementEnd | ||||
|  | ||||
| -- +goose Down | ||||
| @@ -1,12 +0,0 @@ | ||||
| -- +goose Up | ||||
| -- +goose StatementBegin | ||||
| alter table videos | ||||
|     add release_date date null; | ||||
|  | ||||
| -- +goose StatementEnd | ||||
|  | ||||
| -- +goose Down | ||||
| -- +goose StatementBegin | ||||
| alter table videos | ||||
|     drop release_date; | ||||
| -- +goose StatementEnd | ||||
| @@ -1,11 +0,0 @@ | ||||
| -- +goose Up | ||||
| -- +goose StatementBegin | ||||
| alter table videos | ||||
|     add previewratio FLOAT default  -1.0 null; | ||||
| -- +goose StatementEnd | ||||
|  | ||||
| -- +goose Down | ||||
| -- +goose StatementBegin | ||||
| alter table videos | ||||
|     drop previewratio; | ||||
| -- +goose StatementEnd | ||||
| @@ -15,24 +15,35 @@ func GetPassword() *string { | ||||
| } | ||||
|  | ||||
| type SettingsType struct { | ||||
| 	DarkMode        bool | ||||
| 	Pasword         string | ||||
| 	MediacenterName string | ||||
| 	VideoPath       string | ||||
| 	TVShowPath      string | ||||
| 	DarkMode         bool | ||||
| 	Pasword          string | ||||
| 	Mediacenter_name string | ||||
| 	VideoPath        string | ||||
| } | ||||
|  | ||||
| func LoadSettings() *SettingsType { | ||||
| 	query := "SELECT DarkMode, password, mediacenter_name, video_path, episode_path from settings" | ||||
| 	query := "SELECT DarkMode, password, mediacenter_name, video_path from settings" | ||||
|  | ||||
| 	result := SettingsType{} | ||||
| 	var darkmode uint8 | ||||
| 	type RawSettingsType struct { | ||||
| 		DarkMode         int | ||||
| 		Pasword          string | ||||
| 		Mediacenter_name string | ||||
| 		VideoPath        string | ||||
| 	} | ||||
|  | ||||
| 	err := database.QueryRow(query).Scan(&darkmode, &result.Pasword, &result.MediacenterName, &result.VideoPath, &result.TVShowPath) | ||||
| 	result := RawSettingsType{} | ||||
|  | ||||
| 	err := database.QueryRow(query).Scan(&result.DarkMode, &result.Pasword, &result.Mediacenter_name, &result.VideoPath) | ||||
| 	if err != nil { | ||||
| 		fmt.Println("error while parsing db data: " + err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	result.DarkMode = darkmode != 0 | ||||
| 	return &result | ||||
| 	res := SettingsType{ | ||||
| 		DarkMode:         result.DarkMode != 0, | ||||
| 		Pasword:          result.Pasword, | ||||
| 		Mediacenter_name: result.Mediacenter_name, | ||||
| 		VideoPath:        result.VideoPath, | ||||
| 	} | ||||
|  | ||||
| 	return &res | ||||
| } | ||||
|   | ||||
							
								
								
									
										10
									
								
								apiGo/go.mod
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								apiGo/go.mod
									
									
									
									
									
								
							| @@ -3,12 +3,6 @@ module openmediacenter/apiGo | ||||
| go 1.16 | ||||
|  | ||||
| require ( | ||||
| 	github.com/3d0c/gmf v0.0.0-20210925211039-e278e6e53b16 | ||||
| 	github.com/dgrijalva/jwt-go v3.2.0+incompatible | ||||
| 	github.com/fsnotify/fsnotify v1.5.4 | ||||
| 	github.com/go-sql-driver/mysql v1.6.0 | ||||
| 	github.com/pelletier/go-toml/v2 v2.0.0-beta.3 | ||||
| 	github.com/pressly/goose/v3 v3.1.0 | ||||
| 	golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 | ||||
| 	nhooyr.io/websocket v1.8.7 | ||||
| 	github.com/go-sql-driver/mysql v1.5.0 | ||||
| 	gopkg.in/oauth2.v3 v3.12.0 | ||||
| ) | ||||
|   | ||||
							
								
								
									
										188
									
								
								apiGo/go.sum
									
									
									
									
									
								
							
							
						
						
									
										188
									
								
								apiGo/go.sum
									
									
									
									
									
								
							| @@ -1,104 +1,110 @@ | ||||
| github.com/3d0c/gmf v0.0.0-20210925211039-e278e6e53b16 h1:LX3XWmS88yKgWJcMXb8vusphpDBe9+6LTI9FyHeFFWQ= | ||||
| github.com/3d0c/gmf v0.0.0-20210925211039-e278e6e53b16/go.mod h1:0QMRcUq2JsDECeAq7bj4h79k7XbhtTsrPUQf6G7qfPs= | ||||
| github.com/ClickHouse/clickhouse-go v1.4.5/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= | ||||
| github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4= | ||||
| github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= | ||||
| cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= | ||||
| github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= | ||||
| github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= | ||||
| github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= | ||||
| github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= | ||||
| github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= | ||||
| github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= | ||||
| github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= | ||||
| github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= | ||||
| github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= | ||||
| github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= | ||||
| github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= | ||||
| github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= | ||||
| github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= | ||||
| github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= | ||||
| github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= | ||||
| github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= | ||||
| github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= | ||||
| github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= | ||||
| github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= | ||||
| github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= | ||||
| github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= | ||||
| github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= | ||||
| github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= | ||||
| github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= | ||||
| github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= | ||||
| github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= | ||||
| github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= | ||||
| github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= | ||||
| github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= | ||||
| github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= | ||||
| github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= | ||||
| github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= | ||||
| github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= | ||||
| github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||
| github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | ||||
| github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= | ||||
| github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= | ||||
| github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= | ||||
| github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= | ||||
| github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8= | ||||
| github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= | ||||
| github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0= | ||||
| github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= | ||||
| github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= | ||||
| github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||||
| github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= | ||||
| github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= | ||||
| github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= | ||||
| github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= | ||||
| github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= | ||||
| github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | ||||
| github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= | ||||
| github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= | ||||
| github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= | ||||
| github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= | ||||
| github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= | ||||
| github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= | ||||
| github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= | ||||
| github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= | ||||
| github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= | ||||
| github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= | ||||
| github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= | ||||
| github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= | ||||
| github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU= | ||||
| github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= | ||||
| github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= | ||||
| github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | ||||
| github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= | ||||
| github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= | ||||
| github.com/pelletier/go-toml/v2 v2.0.0-beta.3 h1:PNCTU4naEJ8mKal97P3A2qDU74QRQGlv4FXiL1XDqi4= | ||||
| github.com/pelletier/go-toml/v2 v2.0.0-beta.3/go.mod h1:aNseLYu/uKskg0zpr/kbr2z8yGuWtotWf/0BpGIAL2Y= | ||||
| github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= | ||||
| github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | ||||
| github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||
| github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= | ||||
| github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= | ||||
| github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= | ||||
| github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= | ||||
| github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= | ||||
| github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= | ||||
| github.com/klauspost/compress v1.8.2 h1:Bx0qjetmNjdFXASH02NSAREKpiaDwkO1DRZ3dV2KCcs= | ||||
| github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= | ||||
| github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w= | ||||
| github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= | ||||
| github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= | ||||
| github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= | ||||
| github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs= | ||||
| github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= | ||||
| github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= | ||||
| github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= | ||||
| github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= | ||||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||
| github.com/pressly/goose/v3 v3.1.0 h1:V2Ulfm2XL9GtYNmrPUNFHieimf6diwADyMObnuuR2Mc= | ||||
| github.com/pressly/goose/v3 v3.1.0/go.mod h1:tYsY0oL0yd48jg15POIZfOZiu66mqWpfDd/nJ28KWyU= | ||||
| github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= | ||||
| github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= | ||||
| github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= | ||||
| github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= | ||||
| github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= | ||||
| github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= | ||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||
| github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | ||||
| github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= | ||||
| github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= | ||||
| github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942 h1:t0lM6y/M5IiUZyvbBTcngso8SZEZICH7is9B6g/obVU= | ||||
| github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= | ||||
| github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= | ||||
| github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= | ||||
| github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= | ||||
| github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= | ||||
| golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||
| golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= | ||||
| golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= | ||||
| golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= | ||||
| github.com/tidwall/btree v0.0.0-20170113224114-9876f1454cf0 h1:QnyrPZZvPmR0AtJCxxfCtI1qN+fYpKTKJ/5opWmZ34k= | ||||
| github.com/tidwall/btree v0.0.0-20170113224114-9876f1454cf0/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8= | ||||
| github.com/tidwall/buntdb v1.1.0 h1:H6LzK59KiNjf1nHVPFrYj4Qnl8d8YLBsYamdL8N+Bao= | ||||
| github.com/tidwall/buntdb v1.1.0/go.mod h1:Y39xhcDW10WlyYXeLgGftXVbjtM0QP+/kpz8xl9cbzE= | ||||
| github.com/tidwall/gjson v1.3.2 h1:+7p3qQFaH3fOMXAJSrdZwGKcOO/lYdGS0HqGhPqDdTI= | ||||
| github.com/tidwall/gjson v1.3.2/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= | ||||
| github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb h1:5NSYaAdrnblKByzd7XByQEJVT8+9v0W/tIY0Oo4OwrE= | ||||
| github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb/go.mod h1:lKYYLFIr9OIgdgrtgkZ9zgRxRdvPYsExnYBsEAd8W5M= | ||||
| github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc= | ||||
| github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= | ||||
| github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= | ||||
| github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= | ||||
| github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e h1:+NL1GDIUOKxVfbp2KoJQD9cTQ6dyP2co9q4yzmT9FZo= | ||||
| github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e/go.mod h1:/h+UnNGt0IhNNJLkGikcdcJqm66zGD/uJGMRxK/9+Ao= | ||||
| github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 h1:Otn9S136ELckZ3KKDyCkxapfufrqDqwmGjcHfAyXRrE= | ||||
| github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563/go.mod h1:mLqSmt7Dv/CNneF2wfcChfN1rvapyQr01LGKnKex0DQ= | ||||
| github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= | ||||
| github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= | ||||
| github.com/valyala/fasthttp v1.6.0 h1:uWF8lgKmeaIewWVPwi4GRq2P6+R46IgYZdxWtM+GtEY= | ||||
| github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= | ||||
| github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= | ||||
| github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= | ||||
| github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= | ||||
| github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= | ||||
| github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= | ||||
| github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= | ||||
| github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= | ||||
| github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= | ||||
| github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= | ||||
| github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= | ||||
| github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= | ||||
| github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= | ||||
| github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= | ||||
| github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= | ||||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||
| golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||||
| golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM= | ||||
| golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||
| golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= | ||||
| golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= | ||||
| golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||||
| golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= | ||||
| golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
| golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= | ||||
| golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= | ||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||
| golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= | ||||
| google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= | ||||
| gopkg.in/oauth2.v3 v3.12.0 h1:yOffAPoolH/i2JxwmC+pgtnY3362iPahsDpLXfDFvNg= | ||||
| gopkg.in/oauth2.v3 v3.12.0/go.mod h1:XEYgKqWX095YiPT+Aw5y3tCn+7/FMnlTFKrupgSiJ3I= | ||||
| gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= | ||||
| gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
| gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= | ||||
| gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
| gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= | ||||
| gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= | ||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= | ||||
| nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= | ||||
|   | ||||
| @@ -1,18 +0,0 @@ | ||||
| package housekeeping | ||||
|  | ||||
| import "fmt" | ||||
|  | ||||
| func RunHouseKeepingTasks() { | ||||
| 	fmt.Println("Runnint houskeeping tasks!") | ||||
|  | ||||
| 	fmt.Println("Deduplicating Tags") | ||||
| 	deduplicateTags() | ||||
|  | ||||
| 	fmt.Println("Deduplicating Tags assigned to videos") | ||||
| 	deduplicateVideoTags() | ||||
|  | ||||
| 	fmt.Println("Fix missing video metadata like ratio") | ||||
| 	fixMissingMetadata() | ||||
|  | ||||
| 	fmt.Println("Finished housekeeping") | ||||
| } | ||||
| @@ -1,5 +0,0 @@ | ||||
| package housekeeping | ||||
|  | ||||
| func fixMissingMetadata() { | ||||
| 	// todo | ||||
| } | ||||
| @@ -1,86 +0,0 @@ | ||||
| package housekeeping | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"openmediacenter/apiGo/database" | ||||
| ) | ||||
|  | ||||
| func deduplicateTags() { | ||||
| 	// find all duplicate tags | ||||
|  | ||||
| 	// gives first occurence of duplicate | ||||
| 	query := ` | ||||
| SELECT | ||||
|     tag_name | ||||
| FROM | ||||
|     tags | ||||
| GROUP BY tag_name | ||||
| HAVING COUNT(tag_name) > 1` | ||||
| 	rows := database.Query(query) | ||||
| 	duplicates := []string{} | ||||
| 	if rows != nil { | ||||
| 		for rows.Next() { | ||||
| 			var id string | ||||
| 			err := rows.Scan(&id) | ||||
| 			if err != nil { | ||||
| 				panic(err.Error()) // proper Error handling instead of panic in your app | ||||
| 			} | ||||
| 			duplicates = append(duplicates, id) | ||||
| 		} | ||||
| 	} else { | ||||
| 		// nothing to do | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	fmt.Print("deleting duplicate tag ids: ") | ||||
| 	fmt.Println(duplicates) | ||||
|  | ||||
| 	for _, el := range duplicates { | ||||
| 		query := fmt.Sprintf("SELECT tag_id FROM tags WHERE tag_name='%s'", el) | ||||
| 		rows := database.Query(query) | ||||
| 		ids := []uint32{} | ||||
| 		for rows.Next() { | ||||
| 			var id uint32 | ||||
| 			err := rows.Scan(&id) | ||||
| 			if err != nil { | ||||
| 				panic(err.Error()) // proper Error handling instead of panic in your app | ||||
| 			} | ||||
| 			ids = append(ids, id) | ||||
| 		} | ||||
|  | ||||
| 		// id to copy other data to | ||||
| 		mainid := ids[0] | ||||
|  | ||||
| 		// ids to copy from | ||||
| 		copyids := ids[1:] | ||||
|  | ||||
| 		fmt.Printf("Migrating %s\n", el) | ||||
| 		migrateTags(mainid, copyids) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func migrateTags(destid uint32, sourcids []uint32) { | ||||
| 	querytempl := ` | ||||
| UPDATE video_tags  | ||||
| SET  | ||||
|     tag_id = %d | ||||
| WHERE | ||||
|     tag_id = %d` | ||||
|  | ||||
| 	for _, id := range sourcids { | ||||
| 		err := database.Edit(fmt.Sprintf(querytempl, destid, id)) | ||||
| 		if err != nil { | ||||
| 			fmt.Printf("failed to set id from %d to %d\n", id, destid) | ||||
| 			return | ||||
| 		} | ||||
| 		fmt.Printf("Merged %d into %d\n", id, destid) | ||||
|  | ||||
| 		// now lets delete this tag | ||||
| 		query := fmt.Sprintf(`DELETE FROM tags WHERE tag_id=%d`, id) | ||||
| 		err = database.Edit(query) | ||||
| 		if err != nil { | ||||
| 			fmt.Printf("failed to delete Tag %d", id) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -1,37 +0,0 @@ | ||||
| package housekeeping | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"openmediacenter/apiGo/database" | ||||
| ) | ||||
|  | ||||
| func deduplicateVideoTags() { | ||||
| 	// gives first occurence of duplicate | ||||
| 	query := ` | ||||
| SELECT | ||||
|     tag_id, video_id, count(tag_id) | ||||
| FROM | ||||
|     video_tags | ||||
| GROUP BY tag_id, video_id | ||||
| HAVING COUNT(tag_id) > 1` | ||||
| 	rows := database.Query(query) | ||||
| 	if rows != nil { | ||||
| 		for rows.Next() { | ||||
| 			var tagid uint32 | ||||
| 			var vidid uint32 | ||||
| 			var nr uint32 | ||||
| 			err := rows.Scan(&tagid, &vidid, &nr) | ||||
| 			if err != nil { | ||||
| 				panic(err.Error()) // proper Error handling instead of panic in your app | ||||
| 			} | ||||
|  | ||||
| 			// now lets delete this tag | ||||
| 			query := fmt.Sprintf(`DELETE FROM video_tags WHERE tag_id=%d AND video_id=%d LIMIT %d`, tagid, vidid, nr-1) | ||||
| 			err = database.Edit(query) | ||||
| 			if err != nil { | ||||
| 				fmt.Printf("failed to delete Tag %d + vid %d", tagid, vidid) | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @@ -3,61 +3,62 @@ package main | ||||
| import ( | ||||
| 	"flag" | ||||
| 	"fmt" | ||||
| 	"log" | ||||
| 	"net/http" | ||||
| 	"openmediacenter/apiGo/api" | ||||
| 	api2 "openmediacenter/apiGo/api/api" | ||||
| 	"openmediacenter/apiGo/config" | ||||
| 	"openmediacenter/apiGo/database" | ||||
| 	"openmediacenter/apiGo/housekeeping" | ||||
| 	"openmediacenter/apiGo/static" | ||||
| 	"openmediacenter/apiGo/videoparser" | ||||
| 	"os" | ||||
| 	"os/signal" | ||||
| ) | ||||
|  | ||||
| func main() { | ||||
| 	fmt.Println("init OpenMediaCenter server") | ||||
| 	const port uint16 = 8081 | ||||
| 	errc := make(chan error, 1) | ||||
|  | ||||
| 	housekPTr := flag.Bool("HouseKeeping", false, "Run housekeeping tasks") | ||||
|  | ||||
| 	config.Init() | ||||
| 	port := 8081 | ||||
|  | ||||
| 	db, verbose, pathPrefix := handleCommandLineArguments() | ||||
| 	// todo some verbosity logger or sth | ||||
| 	fmt.Printf("Use verbose output: %t\n", config.GetConfig().General.VerboseLogging) | ||||
| 	fmt.Printf("Videopath prefix: %s\n", config.GetConfig().General.ReindexPrefix) | ||||
|  | ||||
| 	err := database.InitDB() | ||||
| 	if err != nil { | ||||
| 		errc <- err | ||||
| 	} | ||||
| 	fmt.Printf("Use verbose output: %t\n", verbose) | ||||
| 	fmt.Printf("Videopath prefix: %s\n", *pathPrefix) | ||||
|  | ||||
| 	// set pathprefix in database settings object | ||||
| 	database.SettingsVideoPrefix = *pathPrefix | ||||
|  | ||||
| 	database.InitDB(db) | ||||
| 	defer database.Close() | ||||
|  | ||||
| 	// check if we should run the housekeeping tasks | ||||
| 	if *housekPTr { | ||||
| 		housekeeping.RunHouseKeepingTasks() | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	api.AddHandlers() | ||||
|  | ||||
| 	videoparser.SetupSettingsWebsocket() | ||||
| 	videoparser.InitFileWatcher() | ||||
| 	api.AddVideoHandlers() | ||||
| 	api.AddSettingsHandlers() | ||||
| 	api.AddTagHandlers() | ||||
| 	api.AddActorsHandlers() | ||||
| 	api.AddInitHandlers() | ||||
|  | ||||
| 	// add the static files | ||||
| 	static.ServeStaticFiles() | ||||
|  | ||||
| 	// init api | ||||
| 	go func() { | ||||
| 		errc <- api2.ServerInit(port) | ||||
| 	}() | ||||
| 	api.ServerInit() | ||||
|  | ||||
| 	sigs := make(chan os.Signal, 1) | ||||
| 	signal.Notify(sigs, os.Interrupt) | ||||
| 	select { | ||||
| 	case err := <-errc: | ||||
| 		fmt.Printf("failed to serve: %v\n", err) | ||||
| 	case sig := <-sigs: | ||||
| 		fmt.Printf("terminating server: %v\n", sig) | ||||
| 	} | ||||
| 	fmt.Printf("OpenMediacenter server up and running on port %d\n", port) | ||||
| 	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil)) | ||||
| } | ||||
|  | ||||
| func handleCommandLineArguments() (*database.DatabaseConfig, bool, *string) { | ||||
| 	dbhostPtr := flag.String("DBHost", "127.0.0.1", "database host name") | ||||
| 	dbPortPtr := flag.Int("DBPort", 3306, "database port") | ||||
| 	dbUserPtr := flag.String("DBUser", "mediacenteruser", "database username") | ||||
| 	dbPassPtr := flag.String("DBPassword", "mediapassword", "database username") | ||||
| 	dbNamePtr := flag.String("DBName", "mediacenter", "database name") | ||||
|  | ||||
| 	verbosePtr := flag.Bool("v", true, "Verbose log output") | ||||
|  | ||||
| 	pathPrefix := flag.String("ReindexPrefix", "/var/www/openmediacenter", "Prefix path for videos to reindex") | ||||
|  | ||||
| 	flag.Parse() | ||||
|  | ||||
| 	return &database.DatabaseConfig{ | ||||
| 		DBHost:     *dbhostPtr, | ||||
| 		DBPort:     *dbPortPtr, | ||||
| 		DBUser:     *dbUserPtr, | ||||
| 		DBPassword: *dbPassPtr, | ||||
| 		DBName:     *dbNamePtr, | ||||
| 	}, *verbosePtr, pathPrefix | ||||
| } | ||||
|   | ||||
| @@ -1,58 +0,0 @@ | ||||
| package videoparser | ||||
|  | ||||
| import ( | ||||
| 	"github.com/fsnotify/fsnotify" | ||||
| 	"log" | ||||
| 	"openmediacenter/apiGo/config" | ||||
| 	"openmediacenter/apiGo/database" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| func InitFileWatcher() { | ||||
| 	watcher, err := fsnotify.NewWatcher() | ||||
| 	if err != nil { | ||||
| 		log.Fatal("NewWatcher failed: ", err) | ||||
| 	} | ||||
|  | ||||
| 	mSettings, _, _ := database.GetSettings() | ||||
| 	vidFolder := config.GetConfig().General.ReindexPrefix + mSettings.VideoPath | ||||
| 	epsfolder := config.GetConfig().General.ReindexPrefix + mSettings.EpisodePath | ||||
|  | ||||
| 	defer watcher.Close() | ||||
| 	go func() { | ||||
| 		for { | ||||
| 			select { | ||||
| 			case event, ok := <-watcher.Events: | ||||
| 				if !ok { | ||||
| 					return | ||||
| 				} | ||||
| 				// start new reindex | ||||
| 				// (may be optimized by checking here if added file is video | ||||
| 				// and start reindex just for one file) | ||||
| 				if strings.Contains(event.Name, vidFolder) { | ||||
| 					StartReindex() | ||||
| 				} else if strings.Contains(event.Name, epsfolder) { | ||||
| 					StartTVShowReindex() | ||||
| 				} else { | ||||
| 					log.Printf("Event in wrong folder: %s %s\n", event.Name, event.Op) | ||||
| 				} | ||||
| 			case err, ok := <-watcher.Errors: | ||||
| 				if !ok { | ||||
| 					return | ||||
| 				} | ||||
| 				log.Println("error:", err) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 	}() | ||||
|  | ||||
| 	err = watcher.Add(vidFolder) | ||||
| 	if err != nil { | ||||
| 		log.Println("Adding of file watcher failed: ", err) | ||||
| 	} | ||||
|  | ||||
| 	err = watcher.Add(epsfolder) | ||||
| 	if err != nil { | ||||
| 		log.Println("Adding of file watcher failed: ", err) | ||||
| 	} | ||||
| } | ||||
| @@ -1,31 +0,0 @@ | ||||
| package videoparser | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| ) | ||||
|  | ||||
| func AppendMessage(message string) { | ||||
| 	msger := TextMessage{ | ||||
| 		MessageBase: MessageBase{Action: "message"}, | ||||
| 		Message:     message, | ||||
| 	} | ||||
| 	marshal, err := json.Marshal(msger) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	IndexSender.Publish(marshal) | ||||
| } | ||||
|  | ||||
| func SendEvent(message string) { | ||||
| 	msger := ReindexEvent{ | ||||
| 		MessageBase: MessageBase{Action: "reindexAction"}, | ||||
| 		Event:       message, | ||||
| 	} | ||||
| 	marshal, err := json.Marshal(msger) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	IndexSender.Publish(marshal) | ||||
| } | ||||
							
								
								
									
										337
									
								
								apiGo/videoparser/ReIndex.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										337
									
								
								apiGo/videoparser/ReIndex.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,337 @@ | ||||
| package videoparser | ||||
|  | ||||
| import ( | ||||
| 	"database/sql" | ||||
| 	"encoding/base64" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"openmediacenter/apiGo/api/types" | ||||
| 	"openmediacenter/apiGo/database" | ||||
| 	"openmediacenter/apiGo/videoparser/tmdb" | ||||
| 	"os/exec" | ||||
| 	"regexp" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| var mSettings types.SettingsType | ||||
| var mExtDepsAvailable *ExtDependencySupport | ||||
|  | ||||
| // default Tag ids | ||||
| const ( | ||||
| 	FullHd     = 2 | ||||
| 	Hd         = 4 | ||||
| 	LowQuality = 3 | ||||
| ) | ||||
|  | ||||
| type ExtDependencySupport struct { | ||||
| 	FFMpeg    bool | ||||
| 	MediaInfo bool | ||||
| } | ||||
|  | ||||
| type VideoAttributes struct { | ||||
| 	Duration float32 | ||||
| 	FileSize uint | ||||
| 	Width    uint | ||||
| } | ||||
|  | ||||
| func ReIndexVideos(path []string, sett types.SettingsType) { | ||||
| 	mSettings = sett | ||||
| 	// check if the extern dependencies are available | ||||
| 	mExtDepsAvailable = checkExtDependencySupport() | ||||
| 	fmt.Printf("FFMPEG support: %t\n", mExtDepsAvailable.FFMpeg) | ||||
| 	fmt.Printf("MediaInfo support: %t\n", mExtDepsAvailable.MediaInfo) | ||||
|  | ||||
| 	// filter out those urls which are already existing in db | ||||
| 	nonExisting := filterExisting(path) | ||||
|  | ||||
| 	fmt.Printf("There are %d videos not existing in db.\n", len(*nonExisting)) | ||||
|  | ||||
| 	for _, s := range *nonExisting { | ||||
| 		processVideo(s) | ||||
| 	} | ||||
|  | ||||
| 	AppendMessageBuffer("reindex finished successfully!") | ||||
|  | ||||
| 	contentAvailable = false | ||||
| 	fmt.Println("Reindexing finished!") | ||||
| } | ||||
|  | ||||
| // filter those entries from array which are already existing! | ||||
| func filterExisting(paths []string) *[]string { | ||||
| 	var nameStr string | ||||
|  | ||||
| 	// build the query string with files on disk | ||||
| 	for i, s := range paths { | ||||
| 		// escape ' in url name | ||||
| 		s = strings.Replace(s, "'", "\\'", -1) | ||||
| 		nameStr += "SELECT '" + s + "' " | ||||
|  | ||||
| 		// if first index add as url | ||||
| 		if i == 0 { | ||||
| 			nameStr += "AS url " | ||||
| 		} | ||||
|  | ||||
| 		// if not last index add union all | ||||
| 		if i != len(paths)-1 { | ||||
| 			nameStr += "UNION ALL " | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	query := fmt.Sprintf("SELECT * FROM (%s) urls WHERE urls.url NOT IN(SELECT movie_url FROM videos)", nameStr) | ||||
| 	rows := database.Query(query) | ||||
|  | ||||
| 	var resultarr []string | ||||
| 	// parse the result rows into a array | ||||
| 	for rows.Next() { | ||||
| 		var url string | ||||
| 		err := rows.Scan(&url) | ||||
| 		if err != nil { | ||||
| 			continue | ||||
| 		} | ||||
| 		resultarr = append(resultarr, url) | ||||
| 	} | ||||
| 	rows.Close() | ||||
|  | ||||
| 	return &resultarr | ||||
| } | ||||
|  | ||||
| func processVideo(fileNameOrig string) { | ||||
| 	fmt.Printf("Processing %s video-", fileNameOrig) | ||||
|  | ||||
| 	// match the file extension | ||||
| 	r, _ := regexp.Compile(`\.[a-zA-Z0-9]+$`) | ||||
| 	fileName := r.ReplaceAllString(fileNameOrig, "") | ||||
|  | ||||
| 	// match the year and cut year from name | ||||
| 	year, fileName := matchYear(fileName) | ||||
|  | ||||
| 	fmt.Printf("The Video %s doesn't exist! Adding it to database.\n", fileName) | ||||
| 	addVideo(fileName, fileNameOrig, year) | ||||
| } | ||||
|  | ||||
| // add a video to the database | ||||
| func addVideo(videoName string, fileName string, year int) { | ||||
| 	var ppic *string | ||||
| 	var poster *string | ||||
| 	var tmdbData *tmdb.VideoTMDB | ||||
| 	var err error | ||||
|  | ||||
| 	// initialize defaults | ||||
| 	vidAtr := &VideoAttributes{ | ||||
| 		Duration: 0, | ||||
| 		FileSize: 0, | ||||
| 		Width:    0, | ||||
| 	} | ||||
|  | ||||
| 	if mExtDepsAvailable.FFMpeg { | ||||
| 		ppic, err = parseFFmpegPic(fileName) | ||||
| 		if err != nil { | ||||
| 			fmt.Printf("FFmpeg error occured: %s\n", err.Error()) | ||||
| 		} else { | ||||
| 			fmt.Println("successfully extracted thumbnail!!") | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if mExtDepsAvailable.MediaInfo { | ||||
| 		atr := getVideoAttributes(fileName) | ||||
| 		if atr != nil { | ||||
| 			vidAtr = atr | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// if TMDB grabbing is enabled serach in api for video... | ||||
| 	if mSettings.TMDBGrabbing { | ||||
| 		tmdbData = tmdb.SearchVideo(videoName, year) | ||||
| 		if tmdbData != nil { | ||||
| 			// reassign parsed pic as poster | ||||
| 			poster = ppic | ||||
| 			// and tmdb pic as thumbnail | ||||
| 			ppic = &tmdbData.Thumbnail | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	query := `INSERT INTO videos(movie_name,movie_url,poster,thumbnail,quality,length) VALUES (?,?,?,?,?,?)` | ||||
| 	err, insertId := database.Insert(query, videoName, fileName, poster, ppic, vidAtr.Width, vidAtr.Duration) | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("Failed to insert video into db: %s\n", err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// add default tags | ||||
| 	if vidAtr.Width != 0 { | ||||
| 		insertSizeTag(vidAtr.Width, uint(insertId)) | ||||
| 	} | ||||
|  | ||||
| 	// add tmdb tags | ||||
| 	if mSettings.TMDBGrabbing && tmdbData != nil { | ||||
| 		insertTMDBTags(tmdbData.GenreIds, insertId) | ||||
| 	} | ||||
|  | ||||
| 	AppendMessageBuffer(fmt.Sprintf("%s - added!", videoName)) | ||||
| } | ||||
|  | ||||
| func matchYear(fileName string) (int, string) { | ||||
| 	r, _ := regexp.Compile(`\([0-9]{4}?\)`) | ||||
| 	years := r.FindAllString(fileName, -1) | ||||
| 	if len(years) == 0 { | ||||
| 		return -1, fileName | ||||
| 	} | ||||
| 	yearStr := years[len(years)-1] | ||||
| 	// get last year occurance and cut first and last char | ||||
| 	year, err := strconv.Atoi(yearStr[1 : len(yearStr)-1]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return -1, fileName | ||||
| 	} | ||||
|  | ||||
| 	// cut out year from filename | ||||
| 	return year, r.ReplaceAllString(fileName, "") | ||||
| } | ||||
|  | ||||
| // parse the thumbail picture from video file | ||||
| func parseFFmpegPic(fileName string) (*string, error) { | ||||
| 	app := "ffmpeg" | ||||
|  | ||||
| 	cmd := exec.Command(app, | ||||
| 		"-hide_banner", | ||||
| 		"-loglevel", "panic", | ||||
| 		"-ss", "00:04:00", | ||||
| 		"-i", mSettings.VideoPath+fileName, | ||||
| 		"-vframes", "1", | ||||
| 		"-q:v", "2", | ||||
| 		"-f", "singlejpeg", | ||||
| 		"pipe:1") | ||||
|  | ||||
| 	stdout, err := cmd.Output() | ||||
|  | ||||
| 	if err != nil { | ||||
| 		fmt.Println(err.Error()) | ||||
| 		fmt.Println(string(err.(*exec.ExitError).Stderr)) | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	strEncPic := base64.StdEncoding.EncodeToString(stdout) | ||||
| 	if strEncPic == "" { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 	backpic64 := fmt.Sprintf("data:image/jpeg;base64,%s", strEncPic) | ||||
|  | ||||
| 	return &backpic64, nil | ||||
| } | ||||
|  | ||||
| func getVideoAttributes(fileName string) *VideoAttributes { | ||||
| 	app := "mediainfo" | ||||
|  | ||||
| 	arg0 := mSettings.VideoPath + fileName | ||||
| 	arg1 := "--Output=JSON" | ||||
|  | ||||
| 	cmd := exec.Command(app, arg1, "-f", arg0) | ||||
| 	stdout, err := cmd.Output() | ||||
|  | ||||
| 	var t struct { | ||||
| 		Media struct { | ||||
| 			Track []struct { | ||||
| 				Duration string | ||||
| 				FileSize string | ||||
| 				Width    string | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	err = json.Unmarshal(stdout, &t) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		fmt.Println(err.Error()) | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	duration, err := strconv.ParseFloat(t.Media.Track[0].Duration, 32) | ||||
| 	filesize, err := strconv.Atoi(t.Media.Track[0].FileSize) | ||||
| 	width, err := strconv.Atoi(t.Media.Track[1].Width) | ||||
|  | ||||
| 	ret := VideoAttributes{ | ||||
| 		Duration: float32(duration), | ||||
| 		FileSize: uint(filesize), | ||||
| 		Width:    uint(width), | ||||
| 	} | ||||
|  | ||||
| 	return &ret | ||||
| } | ||||
|  | ||||
| func AppendMessageBuffer(message string) { | ||||
| 	messageBuffer = append(messageBuffer, message) | ||||
| } | ||||
|  | ||||
| // ext dependency support check | ||||
| func checkExtDependencySupport() *ExtDependencySupport { | ||||
| 	var extDepsAvailable ExtDependencySupport | ||||
|  | ||||
| 	extDepsAvailable.FFMpeg = commandExists("ffmpeg") | ||||
| 	extDepsAvailable.MediaInfo = commandExists("mediainfo") | ||||
|  | ||||
| 	return &extDepsAvailable | ||||
| } | ||||
|  | ||||
| // check if a specific system command is available | ||||
| func commandExists(cmd string) bool { | ||||
| 	_, err := exec.LookPath(cmd) | ||||
| 	return err == nil | ||||
| } | ||||
|  | ||||
| // insert the default size tags to corresponding video | ||||
| func insertSizeTag(width uint, videoId uint) { | ||||
| 	var tagType uint | ||||
|  | ||||
| 	if width >= 1080 { | ||||
| 		tagType = FullHd | ||||
| 	} else if width >= 720 { | ||||
| 		tagType = Hd | ||||
| 	} else { | ||||
| 		tagType = LowQuality | ||||
| 	} | ||||
|  | ||||
| 	query := fmt.Sprintf("INSERT INTO video_tags(video_id,tag_id) VALUES (%d,%d)", videoId, tagType) | ||||
| 	err := database.Edit(query) | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("Eror occured while adding default Tag: %s\n", err.Error()) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // insert id array of tmdb geners to database | ||||
| func insertTMDBTags(ids []int, videoId int64) { | ||||
| 	genres := tmdb.GetGenres() | ||||
|  | ||||
| 	for _, id := range ids { | ||||
| 		var idGenre *tmdb.TMDBGenre | ||||
| 		for _, genre := range *genres { | ||||
| 			if genre.Id == id { | ||||
| 				idGenre = &genre | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
| 		// skip tag if name couldn't be found | ||||
| 		if idGenre == nil { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		// now we check if the tag we want to add already exists | ||||
| 		tagId := createTagToDB(idGenre.Name) | ||||
|  | ||||
| 		// now we add the tag | ||||
| 		query := fmt.Sprintf("INSERT INTO video_tags(video_id,tag_id) VALUES (%d,%d)", videoId, tagId) | ||||
| 		_ = database.Edit(query) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // returns id of tag or creates it if not existing | ||||
| func createTagToDB(tagName string) int64 { | ||||
| 	query := fmt.Sprintf("SELECT tag_id FROM tags WHERE tag_name = %s", tagName) | ||||
| 	var id int64 | ||||
| 	err := database.QueryRow(query).Scan(&id) | ||||
| 	if err == sql.ErrNoRows { | ||||
| 		// tag doesn't exist -- add it | ||||
| 		query = fmt.Sprintf("INSERT INTO tags (tag_name) VALUES (%s)", tagName) | ||||
| 		err, id = database.Insert(query) | ||||
| 	} | ||||
| 	return id | ||||
| } | ||||
| @@ -1,143 +0,0 @@ | ||||
| package videoparser | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"openmediacenter/apiGo/database" | ||||
| 	"openmediacenter/apiGo/videoparser/tmdb" | ||||
| 	"regexp" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| func startTVShowReindex(files []Show) { | ||||
| 	allTVshows := getAllTVShows() | ||||
|  | ||||
| 	for _, file := range files { | ||||
| 		// insert new TVShow entry if not existing. | ||||
| 		insertShowIfNotExisting(file, allTVshows) | ||||
| 		AppendMessage("Processing show: " + file.Name) | ||||
|  | ||||
| 		insertEpisodesIfNotExisting(file) | ||||
| 	} | ||||
|  | ||||
| 	AppendMessage("reindex finished successfully!") | ||||
| 	SendEvent("stop") | ||||
| } | ||||
|  | ||||
| func insertEpisodesIfNotExisting(show Show) { | ||||
| 	query := "SELECT filename FROM tvshow_episodes JOIN tvshow t on t.id = tvshow_episodes.tvshow_id WHERE t.name=?" | ||||
| 	rows := database.Query(query, show.Name) | ||||
|  | ||||
| 	var dbepisodes []string | ||||
| 	for rows.Next() { | ||||
| 		var filename string | ||||
| 		err := rows.Scan(&filename) | ||||
| 		if err != nil { | ||||
| 			fmt.Println(err.Error()) | ||||
| 		} | ||||
|  | ||||
| 		dbepisodes = append(dbepisodes, filename) | ||||
| 	} | ||||
|  | ||||
| 	// get those episodes that are missing in db | ||||
| 	diff := difference(show.files, dbepisodes) | ||||
|  | ||||
| 	for _, s := range diff { | ||||
| 		AppendMessage("Adding Episode: " + s) | ||||
| 		insertEpisode(s, show.Name) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func insertEpisode(path string, ShowName string) { | ||||
| 	seasonRegex := regexp.MustCompile("S[0-9][0-9]") | ||||
| 	episodeRegex := regexp.MustCompile("E[0-9][0-9]") | ||||
| 	matchENDPattern := regexp.MustCompile(" S[0-9][0-9]E[0-9][0-9].+$") | ||||
|  | ||||
| 	seasonStr := seasonRegex.FindString(path) | ||||
| 	episodeStr := episodeRegex.FindString(path) | ||||
| 	extString := matchENDPattern.FindString(path) | ||||
| 	// handle invalid matches | ||||
| 	if len(seasonStr) != 3 || len(episodeStr) != 3 || len(extString) < 8 { | ||||
| 		fmt.Printf("Error inserting episode: %s  -- %s/%s/%s\n", path, seasonStr, episodeStr, extString) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	name := strings.TrimSuffix(path, extString) | ||||
|  | ||||
| 	season, err := strconv.ParseInt(seasonStr[1:], 10, 8) | ||||
| 	episode, err := strconv.ParseInt(episodeStr[1:], 10, 8) | ||||
| 	if err != nil { | ||||
| 		fmt.Println(err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	query := ` | ||||
| INSERT INTO tvshow_episodes (name, season, poster, tvshow_id, episode, filename) | ||||
| VALUES (?, ?, ?, (SELECT tvshow.id FROM tvshow WHERE tvshow.name=?), ?, ?)` | ||||
| 	err = database.Edit(query, name, season, "", ShowName, episode, path) | ||||
| 	if err != nil { | ||||
| 		fmt.Println(err.Error()) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // difference returns the elements in `a` that aren't in `b`. | ||||
| func difference(a, b []string) []string { | ||||
| 	if b == nil || len(b) == 0 { | ||||
| 		return a | ||||
| 	} | ||||
|  | ||||
| 	mb := make(map[string]struct{}, len(b)) | ||||
| 	for _, x := range b { | ||||
| 		mb[x] = struct{}{} | ||||
| 	} | ||||
| 	var diff []string | ||||
| 	for _, x := range a { | ||||
| 		if _, found := mb[x]; !found { | ||||
| 			diff = append(diff, x) | ||||
| 		} | ||||
| 	} | ||||
| 	return diff | ||||
| } | ||||
|  | ||||
| func insertShowIfNotExisting(show Show, allShows *[]string) { | ||||
| 	// if show already exists return | ||||
| 	fmt.Println(*allShows) | ||||
| 	for _, s := range *allShows { | ||||
| 		if s == show.Name { | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// insert empty thubnail if tmdb fails | ||||
| 	thubnail := "" | ||||
|  | ||||
| 	// load tmdb infos | ||||
| 	tmdbInfo := tmdb.SearchTVShow(show.Name) | ||||
| 	if tmdbInfo != nil { | ||||
| 		thubnail = tmdbInfo.Thumbnail | ||||
| 	} | ||||
|  | ||||
| 	// currently the foldernamme == name which mustn't necessarily be | ||||
| 	query := "INSERT INTO tvshow (name, thumbnail, foldername) VALUES (?, ?, ?)" | ||||
| 	err := database.Edit(query, show.Name, thubnail, show.Name) | ||||
| 	if err != nil { | ||||
| 		fmt.Println(err.Error()) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func getAllTVShows() *[]string { | ||||
| 	query := "SELECT name FROM tvshow" | ||||
| 	rows := database.Query(query) | ||||
|  | ||||
| 	var res []string | ||||
| 	for rows.Next() { | ||||
| 		var show string | ||||
| 		err := rows.Scan(&show) | ||||
| 		if err != nil { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		res = append(res, show) | ||||
| 	} | ||||
|  | ||||
| 	return &res | ||||
| } | ||||
| @@ -1,13 +0,0 @@ | ||||
| package videoparser | ||||
|  | ||||
| import "testing" | ||||
|  | ||||
| func TestDifference(t *testing.T) { | ||||
| 	arr1 := []string{"test1", "test2", "test3"} | ||||
| 	arr2 := []string{"test1", "test3"} | ||||
|  | ||||
| 	res := difference(arr1, arr2) | ||||
| 	if len(res) != 1 || res[0] != "test2" { | ||||
| 		t.Errorf("wrong difference result.") | ||||
| 	} | ||||
| } | ||||
| @@ -1,246 +0,0 @@ | ||||
| package videoparser | ||||
|  | ||||
| import ( | ||||
| 	"database/sql" | ||||
| 	"fmt" | ||||
| 	"openmediacenter/apiGo/api/types" | ||||
| 	"openmediacenter/apiGo/config" | ||||
| 	"openmediacenter/apiGo/database" | ||||
| 	"openmediacenter/apiGo/videoparser/thumbnail" | ||||
| 	"openmediacenter/apiGo/videoparser/tmdb" | ||||
| 	"regexp" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| var mSettings *types.SettingsType | ||||
|  | ||||
| // default Tag ids | ||||
| const ( | ||||
| 	FullHd     = 2 | ||||
| 	Hd         = 4 | ||||
| 	LowQuality = 3 | ||||
| ) | ||||
|  | ||||
| type VideoAttributes struct { | ||||
| 	Duration float32 | ||||
| 	FileSize uint | ||||
| 	Width    uint | ||||
| } | ||||
|  | ||||
| func InitDeps(sett *types.SettingsType) { | ||||
| 	mSettings = sett | ||||
| } | ||||
|  | ||||
| func ReIndexVideos(path []string) { | ||||
| 	// filter out those urls which are already existing in db | ||||
| 	nonExisting := filterExisting(path) | ||||
|  | ||||
| 	fmt.Printf("There are %d videos not existing in db.\n", len(*nonExisting)) | ||||
|  | ||||
| 	for _, s := range *nonExisting { | ||||
| 		ProcessVideo(s) | ||||
| 	} | ||||
|  | ||||
| 	AppendMessage("reindex finished successfully!") | ||||
| 	SendEvent("stop") | ||||
| 	fmt.Println("Reindexing finished!") | ||||
| } | ||||
|  | ||||
| // filter those entries from array which are already existing! | ||||
| func filterExisting(paths []string) *[]string { | ||||
| 	var nameStr string | ||||
|  | ||||
| 	// build the query string with files on disk | ||||
| 	for i, s := range paths { | ||||
| 		// escape ' in url name | ||||
| 		s = strings.Replace(s, "'", "\\'", -1) | ||||
| 		nameStr += "SELECT '" + s + "' " | ||||
|  | ||||
| 		// if first index add as url | ||||
| 		if i == 0 { | ||||
| 			nameStr += "AS url " | ||||
| 		} | ||||
|  | ||||
| 		// if not last index add union all | ||||
| 		if i != len(paths)-1 { | ||||
| 			nameStr += "UNION ALL " | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	query := fmt.Sprintf("SELECT * FROM (%s) urls WHERE urls.url NOT IN(SELECT movie_url FROM videos)", nameStr) | ||||
| 	rows := database.Query(query) | ||||
|  | ||||
| 	var resultarr []string | ||||
| 	// parse the result rows into a array | ||||
| 	for rows.Next() { | ||||
| 		var url string | ||||
| 		err := rows.Scan(&url) | ||||
| 		if err != nil { | ||||
| 			continue | ||||
| 		} | ||||
| 		resultarr = append(resultarr, url) | ||||
| 	} | ||||
| 	rows.Close() | ||||
|  | ||||
| 	return &resultarr | ||||
| } | ||||
|  | ||||
| func ProcessVideo(fileNameOrig string) { | ||||
| 	fmt.Printf("Processing %s video\n", fileNameOrig) | ||||
|  | ||||
| 	// match the file extension | ||||
| 	r := regexp.MustCompile(`\.[a-zA-Z0-9]+$`) | ||||
| 	fileName := r.ReplaceAllString(fileNameOrig, "") | ||||
|  | ||||
| 	// match the year and cut year from name | ||||
| 	year, fileName := matchYear(fileName) | ||||
|  | ||||
| 	fmt.Printf("The Video %s doesn't exist! Adding it to database.\n", fileName) | ||||
| 	addVideo(fileName, fileNameOrig, year) | ||||
| } | ||||
|  | ||||
| // add a video to the database | ||||
| func addVideo(videoName string, fileName string, year int) { | ||||
| 	var ppic *string | ||||
| 	var tmdbData *tmdb.VideoTMDB | ||||
| 	var err error | ||||
| 	var insertid int64 | ||||
|  | ||||
| 	vidFolder := config.GetConfig().General.ReindexPrefix + mSettings.VideoPath | ||||
|  | ||||
| 	// if TMDB grabbing is enabled serach in api for video... | ||||
| 	if mSettings.TMDBGrabbing { | ||||
| 		tmdbData = tmdb.SearchVideo(videoName, year) | ||||
| 	} | ||||
|  | ||||
| 	// parse pic from 4min frame | ||||
| 	ppic, vinfo, ffmpegErr := thumbnail.Parse(vidFolder+fileName, 240) | ||||
|  | ||||
| 	if ffmpegErr == nil { | ||||
| 		if mSettings.TMDBGrabbing && tmdbData != nil { | ||||
| 			// inesert fixed pic ratio what we get from tmdb | ||||
| 			previewRatio := 2 / 3 | ||||
| 			query := `INSERT INTO videos(movie_name,movie_url,poster,thumbnail,quality,previewratio,length,release_date) VALUES (?,?,?,?,?,?,?,?)` | ||||
| 			err, insertid = database.Insert(query, videoName, fileName, ppic, tmdbData.Thumbnail, vinfo.Width, previewRatio, vinfo.Length, tmdbData.ReleaseDate) | ||||
| 		} else { | ||||
| 			previewRatio := float32(vinfo.Height) / float32(vinfo.Width) | ||||
| 			// insert without tmdb info | ||||
| 			query := `INSERT INTO videos(movie_name,movie_url,poster,thumbnail,quality,previewratio,length) VALUES (?,?,?,?,?,?,?)` | ||||
| 			err, insertid = database.Insert(query, videoName, fileName, ppic, ppic, vinfo.Width, previewRatio, vinfo.Length) | ||||
| 		} | ||||
| 	} else { | ||||
| 		fmt.Printf("FFmpeg error occured: %s\n", ffmpegErr.Error()) | ||||
|  | ||||
| 		if mSettings.TMDBGrabbing && tmdbData != nil { | ||||
| 			query := `INSERT INTO videos(movie_name,movie_url,thumbnail,release_date) VALUES (?,?,?,?)` | ||||
| 			err, insertid = database.Insert(query, videoName, fileName, tmdbData.Thumbnail, tmdbData.ReleaseDate) | ||||
| 		} else { | ||||
| 			query := `INSERT INTO videos(movie_name,movie_url) VALUES (?,?)` | ||||
| 			err, insertid = database.Insert(query, videoName, fileName) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("Failed to insert video into db: %s\n", err.Error()) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if ffmpegErr == nil { | ||||
| 		// add default tags | ||||
| 		if vinfo.Width != 0 { | ||||
| 			insertSizeTag(uint(vinfo.Width), uint(insertid)) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// add tmdb tags | ||||
| 	if mSettings.TMDBGrabbing && tmdbData != nil { | ||||
| 		insertTMDBTags(tmdbData.GenreIds, insertid) | ||||
| 	} | ||||
|  | ||||
| 	AppendMessage(fmt.Sprintf("%s - added!", videoName)) | ||||
| } | ||||
|  | ||||
| func matchYear(fileName string) (int, string) { | ||||
| 	r := regexp.MustCompile(`\([0-9]{4}?\)`) | ||||
| 	years := r.FindAllString(fileName, -1) | ||||
| 	if len(years) == 0 { | ||||
| 		return -1, fileName | ||||
| 	} | ||||
| 	yearStr := years[len(years)-1] | ||||
| 	// get last year occurance and cut first and last char | ||||
| 	year, err := strconv.Atoi(yearStr[1 : len(yearStr)-1]) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		return -1, fileName | ||||
| 	} | ||||
|  | ||||
| 	// cut out year from filename | ||||
| 	return year, r.ReplaceAllString(fileName, "") | ||||
| } | ||||
|  | ||||
| // insert the default size tags to corresponding video | ||||
| func insertSizeTag(width uint, videoId uint) { | ||||
| 	var tagType uint | ||||
|  | ||||
| 	if width >= 1080 { | ||||
| 		tagType = FullHd | ||||
| 	} else if width >= 720 { | ||||
| 		tagType = Hd | ||||
| 	} else { | ||||
| 		tagType = LowQuality | ||||
| 	} | ||||
|  | ||||
| 	query := fmt.Sprintf("INSERT INTO video_tags(video_id,tag_id) VALUES (%d,%d)", videoId, tagType) | ||||
| 	err := database.Edit(query) | ||||
| 	if err != nil { | ||||
| 		fmt.Printf("Eror occured while adding default Tag: %s\n", err.Error()) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // insert id array of tmdb geners to database | ||||
| func insertTMDBTags(ids []int, videoId int64) { | ||||
| 	genres := tmdb.GetGenres() | ||||
|  | ||||
| 	for _, id := range ids { | ||||
| 		var idGenre *tmdb.TMDBGenre | ||||
| 		for _, genre := range *genres { | ||||
| 			if genre.Id == id { | ||||
| 				idGenre = &genre | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
| 		// skip tag if name couldn't be found | ||||
| 		if idGenre == nil { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		// now we check if the tag we want to add already exists | ||||
| 		tagId := createTagToDB(idGenre.Name) | ||||
| 		if tagId != -1 { | ||||
| 			// now we add the tag | ||||
| 			query := fmt.Sprintf("INSERT INTO video_tags(video_id,tag_id) VALUES (%d,%d)", videoId, tagId) | ||||
| 			_ = database.Edit(query) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // returns id of tag or creates it if not existing | ||||
| func createTagToDB(tagName string) int64 { | ||||
| 	query := "SELECT tag_id FROM tags WHERE tag_name = ?" | ||||
| 	var id int64 = -1 | ||||
| 	err := database.QueryRow(query, tagName).Scan(&id) | ||||
| 	if err == sql.ErrNoRows { | ||||
| 		// tag doesn't exist -- add it | ||||
| 		query = "INSERT INTO tags (tag_name) VALUES (?)" | ||||
| 		err, id = database.Insert(query, tagName) | ||||
| 		if err != nil { | ||||
| 			fmt.Println(err.Error()) | ||||
| 		} | ||||
| 	} else if err != nil { | ||||
| 		if err != nil { | ||||
| 			fmt.Println(err.Error()) | ||||
| 		} | ||||
| 	} | ||||
| 	return id | ||||
| } | ||||
| @@ -2,136 +2,67 @@ package videoparser | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"io/ioutil" | ||||
| 	"openmediacenter/apiGo/config" | ||||
| 	"openmediacenter/apiGo/database" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| var messageBuffer []string | ||||
| var contentAvailable = false | ||||
|  | ||||
| type StatusMessage struct { | ||||
| 	Messages         []string | ||||
| 	ContentAvailable bool | ||||
| } | ||||
|  | ||||
| func getVideoTypes() []string { | ||||
| 	return []string{".mp4", ".mov", ".mkv", ".flv", ".avi", ".mpeg", ".m4v"} | ||||
| } | ||||
|  | ||||
| func ValidVideoSuffix(filename string) bool { | ||||
| 	validExts := getVideoTypes() | ||||
| 	for _, validExt := range validExts { | ||||
| 		if strings.HasSuffix(filename, validExt) { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| func StartReindex() bool { | ||||
| 	messageBuffer = []string{} | ||||
| 	contentAvailable = true | ||||
|  | ||||
| 	fmt.Println("starting reindex..") | ||||
| 	SendEvent("start") | ||||
| 	AppendMessage("starting reindex..") | ||||
|  | ||||
| 	mSettings, _, _ := database.GetSettings() | ||||
|  | ||||
| 	mSettings := database.GetSettings() | ||||
| 	// add the path prefix to videopath | ||||
| 	vidFolder := config.GetConfig().General.ReindexPrefix + mSettings.VideoPath | ||||
| 	mSettings.VideoPath = mSettings.PathPrefix + mSettings.VideoPath | ||||
|  | ||||
| 	// check if path even exists | ||||
| 	if _, err := os.Stat(vidFolder); os.IsNotExist(err) { | ||||
| 	if _, err := os.Stat(mSettings.VideoPath); os.IsNotExist(err) { | ||||
| 		fmt.Println("Reindex path doesn't exist!") | ||||
| 		AppendMessage(fmt.Sprintf("Reindex path doesn't exist! :%s", vidFolder)) | ||||
| 		SendEvent("stop") | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	filelist, err := ioutil.ReadDir(vidFolder) | ||||
| 	if err != nil { | ||||
| 		fmt.Println(err.Error()) | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	var files []string | ||||
| 	for _, file := range filelist { | ||||
| 		if !file.IsDir() && ValidVideoSuffix(file.Name()) { | ||||
| 			files = append(files, file.Name()) | ||||
| 	err := filepath.Walk(mSettings.VideoPath, func(path string, info os.FileInfo, err error) error { | ||||
| 		if err != nil { | ||||
| 			fmt.Println(err.Error()) | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 		if !info.IsDir() && strings.HasSuffix(info.Name(), ".mp4") { | ||||
| 			files = append(files, info.Name()) | ||||
| 		} | ||||
| 		return nil | ||||
| 	}) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		fmt.Println(err.Error()) | ||||
| 	} | ||||
| 	// start reindex process | ||||
| 	AppendMessage("Starting Reindexing!") | ||||
| 	InitDeps(&mSettings) | ||||
| 	go ReIndexVideos(files) | ||||
| 	AppendMessageBuffer("Starting Reindexing!") | ||||
| 	go ReIndexVideos(files, mSettings) | ||||
| 	return true | ||||
| } | ||||
|  | ||||
| type Show struct { | ||||
| 	Name  string | ||||
| 	files []string | ||||
| } | ||||
|  | ||||
| // StartTVShowReindex reindex dir walks for TVShow reindex | ||||
| func StartTVShowReindex() { | ||||
| 	fmt.Println("starting tvshow reindex..") | ||||
| 	SendEvent("start") | ||||
| 	AppendMessage("starting tvshow reindex...") | ||||
|  | ||||
| 	mSettings, PathPrefix, _ := database.GetSettings() | ||||
| 	// add the path prefix to videopath | ||||
| 	mSettings.EpisodePath = PathPrefix + mSettings.EpisodePath | ||||
|  | ||||
| 	// add slash suffix if not existing | ||||
| 	if !strings.HasSuffix(mSettings.EpisodePath, "/") { | ||||
| 		mSettings.EpisodePath += "/" | ||||
| func GetStatusMessage() *StatusMessage { | ||||
| 	msg := StatusMessage{ | ||||
| 		Messages:         messageBuffer, | ||||
| 		ContentAvailable: contentAvailable, | ||||
| 	} | ||||
|  | ||||
| 	// check if path even exists | ||||
| 	if _, err := os.Stat(mSettings.EpisodePath); os.IsNotExist(err) { | ||||
| 		msg := fmt.Sprintf("Reindex path doesn't exist! :%s", mSettings.EpisodePath) | ||||
| 		fmt.Println(msg) | ||||
| 		AppendMessage(msg) | ||||
| 		SendEvent("stop") | ||||
| 		return | ||||
| 	} | ||||
| 	messageBuffer = []string{} | ||||
|  | ||||
| 	var files []Show | ||||
|  | ||||
| 	filess, err := ioutil.ReadDir(mSettings.EpisodePath) | ||||
| 	if err != nil { | ||||
| 		fmt.Println(err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	for _, file := range filess { | ||||
| 		if file.IsDir() { | ||||
| 			elem := Show{ | ||||
| 				Name:  file.Name(), | ||||
| 				files: nil, | ||||
| 			} | ||||
|  | ||||
| 			fmt.Println(file.Name()) | ||||
|  | ||||
| 			episodefiles, err := ioutil.ReadDir(mSettings.EpisodePath + file.Name()) | ||||
| 			if err != nil { | ||||
| 				fmt.Println(err.Error()) | ||||
| 			} | ||||
|  | ||||
| 			for _, epfile := range episodefiles { | ||||
| 				if ValidVideoSuffix(epfile.Name()) { | ||||
| 					elem.files = append(elem.files, epfile.Name()) | ||||
| 				} | ||||
| 			} | ||||
| 			files = append(files, elem) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if err != nil { | ||||
| 		fmt.Println(err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	// start reindex process | ||||
| 	AppendMessage("Starting Reindexing!") | ||||
| 	go startTVShowReindex(files) | ||||
| 	return &msg | ||||
| } | ||||
|  | ||||
| func StartCleanup() { | ||||
|   | ||||
| @@ -1,135 +0,0 @@ | ||||
| package videoparser | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"nhooyr.io/websocket" | ||||
| 	"sync" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| // subscriber represents a subscriber. | ||||
| // Messages are sent on the msgs channel and if the client | ||||
| // cannot keep up with the messages, closeSlow is called. | ||||
| type subscriber struct { | ||||
| 	msgs      chan []byte | ||||
| 	closeSlow func() | ||||
| } | ||||
|  | ||||
| type ChatSender struct { | ||||
| 	subscribersMu sync.Mutex | ||||
| 	subscribers   map[*subscriber]struct{} | ||||
| } | ||||
|  | ||||
| func newChatSender() *ChatSender { | ||||
| 	return &ChatSender{ | ||||
| 		subscribers: make(map[*subscriber]struct{}), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (t *ChatSender) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||
| 	c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ | ||||
| 		OriginPatterns: []string{"*"}, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		fmt.Println(err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 	defer c.Close(websocket.StatusInternalError, "") | ||||
|  | ||||
| 	err = t.subscribe(r.Context(), c) | ||||
| 	if errors.Is(err, context.Canceled) { | ||||
| 		return | ||||
| 	} | ||||
| 	if websocket.CloseStatus(err) == websocket.StatusNormalClosure || | ||||
| 		websocket.CloseStatus(err) == websocket.StatusGoingAway { | ||||
| 		return | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		fmt.Println(err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (t *ChatSender) subscribe(ctx context.Context, c *websocket.Conn) error { | ||||
| 	ctx = c.CloseRead(ctx) | ||||
|  | ||||
| 	s := &subscriber{ | ||||
| 		msgs: make(chan []byte, 16), | ||||
| 		closeSlow: func() { | ||||
| 			c.Close(websocket.StatusPolicyViolation, "connection too slow to keep up with messages") | ||||
| 		}, | ||||
| 	} | ||||
| 	t.addSubscriber(s) | ||||
| 	defer t.deleteSubscriber(s) | ||||
|  | ||||
| 	for { | ||||
| 		select { | ||||
| 		case msg := <-s.msgs: | ||||
| 			err := writeTimeout(ctx, time.Second*5, c, msg) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		case <-ctx.Done(): | ||||
| 			return ctx.Err() | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type MessageBase struct { | ||||
| 	Action string | ||||
| } | ||||
|  | ||||
| type TextMessage struct { | ||||
| 	MessageBase | ||||
|  | ||||
| 	Message string | ||||
| } | ||||
|  | ||||
| type ReindexEvent struct { | ||||
| 	MessageBase | ||||
|  | ||||
| 	Event string | ||||
| } | ||||
|  | ||||
| func (t *ChatSender) Publish(msg []byte) { | ||||
| 	t.subscribersMu.Lock() | ||||
| 	defer t.subscribersMu.Unlock() | ||||
|  | ||||
| 	for s := range t.subscribers { | ||||
| 		select { | ||||
| 		case s.msgs <- msg: | ||||
| 		default: | ||||
| 			go s.closeSlow() | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| var IndexSender = newChatSender() | ||||
|  | ||||
| func SetupSettingsWebsocket() { | ||||
| 	http.Handle("/subscribe", IndexSender) | ||||
| } | ||||
|  | ||||
| // addSubscriber registers a subscriber. | ||||
| func (t *ChatSender) addSubscriber(s *subscriber) { | ||||
| 	t.subscribersMu.Lock() | ||||
| 	t.subscribers[s] = struct{}{} | ||||
| 	t.subscribersMu.Unlock() | ||||
| } | ||||
|  | ||||
| // deleteSubscriber deletes the given subscriber. | ||||
| func (t *ChatSender) deleteSubscriber(s *subscriber) { | ||||
| 	t.subscribersMu.Lock() | ||||
| 	delete(t.subscribers, s) | ||||
| 	t.subscribersMu.Unlock() | ||||
| } | ||||
|  | ||||
| func writeTimeout(ctx context.Context, timeout time.Duration, c *websocket.Conn, msg []byte) error { | ||||
| 	ctx, cancel := context.WithTimeout(ctx, timeout) | ||||
| 	defer cancel() | ||||
|  | ||||
| 	return c.Write(ctx, websocket.MessageText, msg) | ||||
| } | ||||
| @@ -1,22 +0,0 @@ | ||||
| package thumbnail | ||||
|  | ||||
| import ( | ||||
| 	"encoding/base64" | ||||
| 	"fmt" | ||||
| ) | ||||
|  | ||||
| type VidInfo struct { | ||||
| 	Width     uint32 | ||||
| 	Height    uint32 | ||||
| 	Length    uint64 | ||||
| 	FrameRate float32 | ||||
| 	Size      int64 | ||||
| } | ||||
|  | ||||
| func EncodeBase64(data *[]byte, mimetype string) *string { | ||||
| 	strEncPic := base64.StdEncoding.EncodeToString(*data) | ||||
| 	backpic64 := fmt.Sprintf("data:%s;base64,%s", mimetype, strEncPic) | ||||
|  | ||||
| 	return &backpic64 | ||||
|  | ||||
| } | ||||
| @@ -1,198 +0,0 @@ | ||||
| // +build sharedffmpeg | ||||
|  | ||||
| package thumbnail | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"github.com/3d0c/gmf" | ||||
| 	"io" | ||||
| 	"log" | ||||
| 	"os" | ||||
| ) | ||||
|  | ||||
| func Parse(filename string, time uint64) (*string, *VidInfo, error) { | ||||
| 	dta, inf, err := decodePic(filename, "mjpeg", time) | ||||
| 	if err == nil && dta != nil { | ||||
| 		// base64 encode picture | ||||
| 		enc := EncodeBase64(dta, "image/jpeg") | ||||
| 		return enc, inf, nil | ||||
| 	} else { | ||||
| 		return nil, nil, err | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func decodePic(srcFileName string, encodeExtension string, time uint64) (pic *[]byte, info *VidInfo, err error) { | ||||
| 	var swsctx *gmf.SwsCtx | ||||
|  | ||||
| 	gmf.LogSetLevel(gmf.AV_LOG_PANIC) | ||||
|  | ||||
| 	stat, err := os.Stat(srcFileName) | ||||
| 	if err != nil { | ||||
| 		// file seems to not even exist | ||||
| 		return nil, nil, err | ||||
| 	} | ||||
| 	fileSize := stat.Size() | ||||
|  | ||||
| 	inputCtx, err := gmf.NewInputCtx(srcFileName) | ||||
|  | ||||
| 	if err != nil { | ||||
| 		log.Printf("Error creating context - %s\n", err) | ||||
| 		return nil, nil, err | ||||
| 	} | ||||
| 	defer inputCtx.Free() | ||||
|  | ||||
| 	srcVideoStream, err := inputCtx.GetBestStream(gmf.AVMEDIA_TYPE_VIDEO) | ||||
| 	if err != nil { | ||||
| 		log.Printf("No video stream found in '%s'\n", srcFileName) | ||||
| 		return nil, nil, err | ||||
| 	} | ||||
|  | ||||
| 	encodeCodec, err := gmf.FindEncoder(encodeExtension) | ||||
| 	if err != nil { | ||||
| 		log.Printf("%s\n", err) | ||||
| 		return nil, nil, err | ||||
| 	} | ||||
|  | ||||
| 	cc := gmf.NewCodecCtx(encodeCodec) | ||||
| 	defer gmf.Release(cc) | ||||
|  | ||||
| 	cc.SetTimeBase(gmf.AVR{Num: 1, Den: 1}) | ||||
|  | ||||
| 	cc.SetPixFmt(gmf.AV_PIX_FMT_YUVJ444P).SetWidth(srcVideoStream.CodecPar().Width()).SetHeight(srcVideoStream.CodecPar().Height()) | ||||
| 	if encodeCodec.IsExperimental() { | ||||
| 		cc.SetStrictCompliance(gmf.FF_COMPLIANCE_EXPERIMENTAL) | ||||
| 	} | ||||
|  | ||||
| 	if err := cc.Open(nil); err != nil { | ||||
| 		log.Println(err) | ||||
| 		return nil, nil, err | ||||
| 	} | ||||
| 	defer cc.Free() | ||||
|  | ||||
| 	err = inputCtx.SeekFrameAt(int64(time), srcVideoStream.Index()) | ||||
| 	if err != nil { | ||||
| 		log.Printf("Error while seeking file: %s\n", err.Error()) | ||||
| 		return nil, nil, err | ||||
| 	} | ||||
|  | ||||
| 	// find encodeCodec to decode video | ||||
| 	decodeCodec, err := gmf.FindDecoder(srcVideoStream.CodecPar().CodecId()) | ||||
| 	if err != nil { | ||||
| 		fmt.Println(err) | ||||
| 		return nil, nil, err | ||||
| 	} | ||||
|  | ||||
| 	icc := gmf.NewCodecCtx(decodeCodec) | ||||
| 	defer gmf.Release(icc) | ||||
|  | ||||
| 	// copy stream parameters in codeccontext | ||||
| 	err = srcVideoStream.CodecPar().ToContext(icc) | ||||
| 	if err != nil { | ||||
| 		fmt.Println(err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	// convert source pix_fmt into AV_PIX_FMT_RGBA | ||||
| 	if swsctx, err = gmf.NewSwsCtx(icc.Width(), icc.Height(), icc.PixFmt(), cc.Width(), cc.Height(), cc.PixFmt(), gmf.SWS_BICUBIC); err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	defer swsctx.Free() | ||||
|  | ||||
| 	frameRate := float32(srcVideoStream.GetRFrameRate().AVR().Num) / float32(srcVideoStream.GetRFrameRate().AVR().Den) | ||||
| 	inf := VidInfo{ | ||||
| 		Width:     uint32(icc.Width()), | ||||
| 		Height:    uint32(icc.Height()), | ||||
| 		FrameRate: frameRate, | ||||
| 		Length:    uint64(inputCtx.Duration()), | ||||
| 		Size:      fileSize, | ||||
| 	} | ||||
|  | ||||
| 	info = &inf | ||||
|  | ||||
| 	var ( | ||||
| 		pkt        *gmf.Packet | ||||
| 		frames     []*gmf.Frame | ||||
| 		drain      int = -1 | ||||
| 		frameCount int = 0 | ||||
| 	) | ||||
|  | ||||
| 	for { | ||||
| 		if drain >= 0 { | ||||
| 			break | ||||
| 		} | ||||
|  | ||||
| 		pkt, err = inputCtx.GetNextPacket() | ||||
| 		if err != nil && err != io.EOF { | ||||
| 			if pkt != nil { | ||||
| 				pkt.Free() | ||||
| 			} | ||||
| 			log.Printf("error getting next packet - %s", err) | ||||
| 			break | ||||
| 		} else if err != nil && pkt == nil { | ||||
| 			drain = 0 | ||||
| 		} | ||||
|  | ||||
| 		if pkt != nil && pkt.StreamIndex() != srcVideoStream.Index() { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		frames, err = srcVideoStream.CodecCtx().Decode(pkt) | ||||
|  | ||||
| 		if err != nil { | ||||
| 			log.Printf("Fatal error during decoding - %s\n", err) | ||||
| 			break | ||||
| 		} | ||||
|  | ||||
| 		// Decode() method doesn't treat EAGAIN and EOF as errors | ||||
| 		// it returns empty frames slice instead. Countinue until | ||||
| 		// input EOF or frames received. | ||||
| 		if len(frames) == 0 && drain < 0 { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		if frames, err = gmf.DefaultRescaler(swsctx, frames); err != nil { | ||||
| 			panic(err) | ||||
| 		} | ||||
|  | ||||
| 		packets, err := cc.Encode(frames, drain) | ||||
| 		if len(packets) == 0 { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		if err != nil { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		picdata := packets[0].Data() | ||||
| 		pic = &picdata | ||||
|  | ||||
| 		// cleanup here | ||||
| 		for _, p := range packets { | ||||
| 			p.Free() | ||||
| 		} | ||||
|  | ||||
| 		for i := range frames { | ||||
| 			frames[i].Free() | ||||
| 			frameCount++ | ||||
| 		} | ||||
|  | ||||
| 		if pkt != nil { | ||||
| 			pkt.Free() | ||||
| 			pkt = nil | ||||
| 		} | ||||
|  | ||||
| 		// we only want to encode first picture then exit | ||||
| 		break | ||||
| 	} | ||||
|  | ||||
| 	for i := 0; i < inputCtx.StreamsCnt(); i++ { | ||||
| 		st, err := inputCtx.GetStream(i) | ||||
| 		if err == nil && st != nil { | ||||
| 			st.Free() | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	icc.Free() | ||||
| 	srcVideoStream.Free() | ||||
|  | ||||
| 	return | ||||
| } | ||||
| @@ -1,130 +0,0 @@ | ||||
| // +build !sharedffmpeg | ||||
|  | ||||
| package thumbnail | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"os/exec" | ||||
| 	"strconv" | ||||
| ) | ||||
|  | ||||
| type ExtDependencySupport struct { | ||||
| 	FFMpeg    bool | ||||
| 	MediaInfo bool | ||||
| } | ||||
|  | ||||
| func Parse(filename string, time uint64) (*string, *VidInfo, error) { | ||||
| 	// check if the extern dependencies are available | ||||
| 	mExtDepsAvailable := checkExtDependencySupport() | ||||
| 	fmt.Printf("FFMPEG support: %t\n", mExtDepsAvailable.FFMpeg) | ||||
| 	fmt.Printf("MediaInfo support: %t\n", mExtDepsAvailable.MediaInfo) | ||||
|  | ||||
| 	var pic *string = nil | ||||
| 	var info *VidInfo = nil | ||||
|  | ||||
| 	if mExtDepsAvailable.FFMpeg { | ||||
| 		p, err := parseFFmpegPic(filename, time) | ||||
| 		if err != nil { | ||||
| 			return nil, nil, err | ||||
| 		} | ||||
| 		pic = EncodeBase64(p, "image/jpeg") | ||||
| 	} | ||||
|  | ||||
| 	if mExtDepsAvailable.MediaInfo { | ||||
| 		i, err := getVideoAttributes(filename) | ||||
| 		if err != nil { | ||||
| 			return nil, nil, err | ||||
| 		} | ||||
| 		info = i | ||||
| 	} | ||||
| 	return pic, info, nil | ||||
| } | ||||
|  | ||||
| // check if a specific system command is available | ||||
| func commandExists(cmd string) bool { | ||||
| 	_, err := exec.LookPath(cmd) | ||||
| 	return err == nil | ||||
| } | ||||
|  | ||||
| // ext dependency support check | ||||
| func checkExtDependencySupport() *ExtDependencySupport { | ||||
| 	var extDepsAvailable ExtDependencySupport | ||||
|  | ||||
| 	extDepsAvailable.FFMpeg = commandExists("ffmpeg") | ||||
| 	extDepsAvailable.MediaInfo = commandExists("mediainfo") | ||||
|  | ||||
| 	return &extDepsAvailable | ||||
| } | ||||
|  | ||||
| func secToString(time uint64) string { | ||||
| 	return fmt.Sprintf("%02d:%02d:%02d", time/3600, (time/60)%60, time%60) | ||||
| } | ||||
|  | ||||
| // parse the thumbail picture from video file | ||||
| func parseFFmpegPic(path string, time uint64) (*[]byte, error) { | ||||
| 	app := "ffmpeg" | ||||
|  | ||||
| 	cmd := exec.Command(app, | ||||
| 		"-hide_banner", | ||||
| 		"-loglevel", "panic", | ||||
| 		"-ss", secToString(time), | ||||
| 		"-i", path, | ||||
| 		"-vframes", "1", | ||||
| 		"-q:v", "2", | ||||
| 		"-f", "singlejpeg", | ||||
| 		"pipe:1") | ||||
|  | ||||
| 	stdout, err := cmd.Output() | ||||
|  | ||||
| 	if err != nil { | ||||
| 		fmt.Println(err.Error()) | ||||
| 		fmt.Println(string(err.(*exec.ExitError).Stderr)) | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return &stdout, nil | ||||
| } | ||||
|  | ||||
| func getVideoAttributes(path string) (*VidInfo, error) { | ||||
| 	app := "mediainfo" | ||||
|  | ||||
| 	arg0 := path | ||||
| 	arg1 := "--Output=JSON" | ||||
|  | ||||
| 	cmd := exec.Command(app, arg1, "-f", arg0) | ||||
| 	stdout, err := cmd.Output() | ||||
|  | ||||
| 	var t struct { | ||||
| 		Media struct { | ||||
| 			Track []struct { | ||||
| 				Duration string | ||||
| 				FileSize string | ||||
| 				Width    string | ||||
| 				Height   string | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	err = json.Unmarshal(stdout, &t) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	duration, err := strconv.ParseFloat(t.Media.Track[0].Duration, 32) | ||||
| 	filesize, err := strconv.Atoi(t.Media.Track[0].FileSize) | ||||
| 	width, err := strconv.Atoi(t.Media.Track[1].Width) | ||||
| 	height, err := strconv.Atoi(t.Media.Track[1].Height) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	ret := VidInfo{ | ||||
| 		Length:    uint64(duration), | ||||
| 		Size:      int64(filesize), | ||||
| 		Width:     uint32(width), | ||||
| 		Height:    uint32(height), | ||||
| 		FrameRate: 0, | ||||
| 	} | ||||
|  | ||||
| 	return &ret, nil | ||||
| } | ||||
| @@ -15,50 +15,27 @@ const baseUrl = "https://api.themoviedb.org/3/" | ||||
| const pictureBase = "https://image.tmdb.org/t/p/w500" | ||||
|  | ||||
| type VideoTMDB struct { | ||||
| 	Thumbnail   string | ||||
| 	Overview    string | ||||
| 	Title       string | ||||
| 	ReleaseDate string | ||||
| 	GenreIds    []int | ||||
| } | ||||
|  | ||||
| type TVShowTMDB struct { | ||||
| 	Thumbnail string | ||||
| 	Overview  string | ||||
| 	Title     string | ||||
| 	GenreIds  []int | ||||
| } | ||||
|  | ||||
| type tmdbVidResult struct { | ||||
| 	PosterPath       *string `json:"poster_path"` | ||||
| 	Adult            bool    `json:"adult"` | ||||
| 	Overview         string  `json:"overview"` | ||||
| 	ReleaseDate      string  `json:"release_date"` | ||||
| 	GenreIds         []int   `json:"genre_ids"` | ||||
| 	Id               int     `json:"id"` | ||||
| 	OriginalTitle    string  `json:"original_title"` | ||||
| 	OriginalLanguage string  `json:"original_language"` | ||||
| 	Title            string  `json:"title"` | ||||
| 	BackdropPath     *string `json:"backdrop_path"` | ||||
| 	Popularity       int     `json:"popularity"` | ||||
| 	VoteCount        int     `json:"vote_count"` | ||||
| 	Video            bool    `json:"video"` | ||||
| 	VoteAverage      int     `json:"vote_average"` | ||||
| } | ||||
|  | ||||
| type tmdbTvResult struct { | ||||
| 	PosterPath       *string  `json:"poster_path"` | ||||
| 	Popularity       int      `json:"popularity"` | ||||
| 	Id               int      `json:"id"` | ||||
| 	BackdropPath     *string  `json:"backdrop_path"` | ||||
| 	VoteAverage      int      `json:"vote_average"` | ||||
| 	Overview         string   `json:"overview"` | ||||
| 	FirstAirDate     string   `json:"first_air_date"` | ||||
| 	OriginCountry    []string `json:"origin_country"` | ||||
| 	GenreIds         []int    `json:"genre_ids"` | ||||
| 	OriginalLanguage string   `json:"original_language"` | ||||
| 	VoteCount        int      `json:"vote_count"` | ||||
| 	Name             string   `json:"name"` | ||||
| 	OriginalName     string   `json:"original_name"` | ||||
| 	Poster_path       string | ||||
| 	Adult             bool | ||||
| 	Overview          string | ||||
| 	Release_date      string | ||||
| 	Genre_ids         []int | ||||
| 	Id                int | ||||
| 	Original_title    string | ||||
| 	Original_language string | ||||
| 	Title             string | ||||
| 	Backdrop_path     string | ||||
| 	Popularity        int | ||||
| 	Vote_count        int | ||||
| 	Video             bool | ||||
| 	Vote_average      int | ||||
| } | ||||
|  | ||||
| type TMDBGenre struct { | ||||
| @@ -97,7 +74,7 @@ func SearchVideo(MovieName string, year int) *VideoTMDB { | ||||
| 	if year != -1 { | ||||
| 		for _, result := range t.Results { | ||||
| 			r, _ := regexp.Compile(fmt.Sprintf(`^%d-[0-9]{2}?-[0-9]{2}?$`, year)) | ||||
| 			if r.MatchString(result.ReleaseDate) { | ||||
| 			if r.MatchString(result.Release_date) { | ||||
| 				tmdbVid = result | ||||
| 				// continue parsing | ||||
| 				goto cont | ||||
| @@ -112,70 +89,22 @@ func SearchVideo(MovieName string, year int) *VideoTMDB { | ||||
| 	// continue label | ||||
| cont: | ||||
|  | ||||
| 	var thumbnail = "" | ||||
| 	if tmdbVid.PosterPath != nil { | ||||
| 		pic := fetchPoster(*tmdbVid.PosterPath) | ||||
| 		if pic != nil { | ||||
| 			thumbnail = *pic | ||||
| 		} | ||||
| 	} | ||||
| 	thumbnail := fetchPoster(tmdbVid) | ||||
|  | ||||
| 	result := VideoTMDB{ | ||||
| 		Thumbnail:   thumbnail, | ||||
| 		Overview:    tmdbVid.Overview, | ||||
| 		Title:       tmdbVid.Title, | ||||
| 		ReleaseDate: tmdbVid.ReleaseDate, | ||||
| 		GenreIds:    tmdbVid.GenreIds, | ||||
| 		Thumbnail: *thumbnail, | ||||
| 		Overview:  tmdbVid.Overview, | ||||
| 		Title:     tmdbVid.Title, | ||||
| 		GenreIds:  tmdbVid.Genre_ids, | ||||
| 	} | ||||
|  | ||||
| 	return &result | ||||
| } | ||||
|  | ||||
| func SearchTVShow(Name string) *TVShowTMDB { | ||||
| 	fmt.Printf("Searching TMDB for: TVShow: %s\n", Name) | ||||
| 	queryURL := fmt.Sprintf("%ssearch/tv?api_key=%s&query=%s", baseUrl, apiKey, url.QueryEscape(Name)) | ||||
| 	resp, err := http.Get(queryURL) | ||||
| 	if err != nil { | ||||
| 		fmt.Println(err.Error()) | ||||
| 		return nil | ||||
| 	} | ||||
| func fetchPoster(vid tmdbVidResult) *string { | ||||
| 	url := fmt.Sprintf("%s%s", pictureBase, vid.Poster_path) | ||||
|  | ||||
| 	body, err := ioutil.ReadAll(resp.Body) | ||||
| 	if err != nil { | ||||
| 		fmt.Println(err.Error()) | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	var t struct { | ||||
| 		Results []tmdbTvResult `json:"results"` | ||||
| 	} | ||||
| 	err = json.Unmarshal(body, &t) | ||||
|  | ||||
| 	fmt.Println(len(t.Results)) | ||||
|  | ||||
| 	if len(t.Results) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	res := TVShowTMDB{ | ||||
| 		Thumbnail: "", | ||||
| 		Overview:  t.Results[0].Overview, | ||||
| 		GenreIds:  t.Results[0].GenreIds, | ||||
| 	} | ||||
|  | ||||
| 	if t.Results[0].PosterPath != nil { | ||||
| 		pic := fetchPoster(*t.Results[0].PosterPath) | ||||
| 		if pic != nil { | ||||
| 			res.Thumbnail = *pic | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return &res | ||||
| } | ||||
|  | ||||
| func fetchPoster(posterPath string) *string { | ||||
| 	posterURL := fmt.Sprintf("%s%s", pictureBase, posterPath) | ||||
| 	resp, err := http.Get(posterURL) | ||||
| 	resp, err := http.Get(url) | ||||
| 	if err != nil { | ||||
| 		fmt.Println(err.Error()) | ||||
| 		return nil | ||||
| @@ -194,8 +123,8 @@ func fetchPoster(posterPath string) *string { | ||||
| var tmdbGenres *[]TMDBGenre | ||||
|  | ||||
| func fetchGenres() *[]TMDBGenre { | ||||
| 	posterURL := fmt.Sprintf("%sgenre/movie/list?api_key=%s", baseUrl, apiKey) | ||||
| 	resp, err := http.Get(posterURL) | ||||
| 	url := fmt.Sprintf("%sgenre/movie/list?api_key=%s", baseUrl, apiKey) | ||||
| 	resp, err := http.Get(url) | ||||
| 	if err != nil { | ||||
| 		fmt.Println(err.Error()) | ||||
| 		return nil | ||||
| @@ -207,14 +136,10 @@ func fetchGenres() *[]TMDBGenre { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	type RespType struct { | ||||
| 		Genres []TMDBGenre | ||||
| 	} | ||||
|  | ||||
| 	var t RespType | ||||
| 	var t []TMDBGenre | ||||
| 	err = json.Unmarshal(body, &t) | ||||
|  | ||||
| 	return &t.Genres | ||||
| 	return &t | ||||
| } | ||||
|  | ||||
| func GetGenres() *[]TMDBGenre { | ||||
|   | ||||
							
								
								
									
										51
									
								
								database.sql
									
									
									
									
									
								
							
							
						
						
									
										51
									
								
								database.sql
									
									
									
									
									
								
							| @@ -9,12 +9,12 @@ create table if not exists actors | ||||
|  | ||||
| create table if not exists settings | ||||
| ( | ||||
|     video_path       varchar(255)                          null, | ||||
|     episode_path     varchar(255)                          null, | ||||
|     password         varchar(32) default '-1'              null, | ||||
|     mediacenter_name varchar(32) default 'OpenMediaCenter' null, | ||||
|     TMDB_grabbing    tinyint                               null, | ||||
|     DarkMode         tinyint     default 0                 null | ||||
|     video_path       varchar(255)      null, | ||||
|     episode_path     varchar(255)      null, | ||||
|     password         varchar(32)       null, | ||||
|     mediacenter_name varchar(32)       null, | ||||
|     TMDB_grabbing    tinyint           null, | ||||
|     DarkMode         tinyint default 0 null | ||||
| ); | ||||
|  | ||||
| create table if not exists tags | ||||
| @@ -24,41 +24,18 @@ create table if not exists tags | ||||
|     tag_name varchar(50) null | ||||
| ); | ||||
|  | ||||
| create table if not exists tvshow | ||||
| ( | ||||
|     name       varchar(100) null, | ||||
|     thumbnail  mediumblob   null, | ||||
|     id         int auto_increment | ||||
|         primary key, | ||||
|     foldername varchar(100) null | ||||
| ); | ||||
|  | ||||
| create table if not exists tvshow_episodes | ||||
| ( | ||||
|     id        int auto_increment | ||||
|         primary key, | ||||
|     name      varchar(100) null, | ||||
|     season    int          null, | ||||
|     poster    mediumblob   null, | ||||
|     tvshow_id int          null, | ||||
|     episode   int          null, | ||||
|     filename  varchar(100) null, | ||||
|     constraint tvshow_episodes_tvshow_id_fk | ||||
|         foreign key (tvshow_id) references tvshow (id) | ||||
| ); | ||||
|  | ||||
| create table if not exists videos | ||||
| ( | ||||
|     movie_id    int auto_increment | ||||
|         primary key, | ||||
|     movie_name  varchar(200)                         null, | ||||
|     movie_url   varchar(250)                         null, | ||||
|     thumbnail   mediumblob                           null, | ||||
|     poster      mediumblob                           null, | ||||
|     likes       int      default 0                   null, | ||||
|     quality     int      default 0                   null, | ||||
|     length      int      default 0                   null comment 'in seconds', | ||||
|     create_date datetime default current_timestamp() null | ||||
|     movie_name  varchar(200)                       null, | ||||
|     movie_url   varchar(250)                       null, | ||||
|     thumbnail   mediumblob                         null, | ||||
|     likes       int      default 0                 null, | ||||
|     create_date datetime default CURRENT_TIMESTAMP null, | ||||
|     quality     int                                null, | ||||
|     length      int                                null comment 'in seconds', | ||||
|     poster      mediumblob                         null | ||||
| ); | ||||
|  | ||||
| create table if not exists actors_videos | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| Package: OpenMediaCenter | ||||
| Depends: nginx, mariadb-server, libffmpeg-ocaml | ||||
| Depends: nginx, mariadb-server, mediainfo | ||||
| Section: web | ||||
| Priority: optional | ||||
| Architecture: all | ||||
|   | ||||
| @@ -6,6 +6,10 @@ ln -sf /etc/nginx/sites-available/OpenMediaCenter.conf /etc/nginx/sites-enabled/ | ||||
| mysql -uroot -pPASS -e "CREATE DATABASE IF NOT EXISTS mediacenter;" | ||||
| mysql -uroot -pPASS -e "CREATE USER IF NOT EXISTS 'mediacenteruser'@'localhost' IDENTIFIED BY 'mediapassword';" | ||||
| mysql -uroot -pPASS -e "GRANT ALL PRIVILEGES ON mediacenter . * TO 'mediacenteruser'@'localhost';" | ||||
| mysql -u mediacenteruser -pmediapassword mediacenter < /tmp/openmediacenter.sql | ||||
|  | ||||
| # removed unused sql style file | ||||
| rm /tmp/openmediacenter.sql | ||||
|  | ||||
| # correct user rights | ||||
| chown -R www-data:www-data /var/www/openmediacenter | ||||
|   | ||||
| @@ -13,15 +13,7 @@ server { | ||||
|         try_files $uri /index.html; | ||||
|     } | ||||
|  | ||||
|     location ~* ^/(api) { | ||||
|         client_max_body_size 10G; | ||||
|     location ~* ^/(api/|token) { | ||||
|         proxy_pass http://127.0.0.1:8081; | ||||
|     } | ||||
|     location /subscribe { | ||||
|         proxy_pass http://127.0.0.1:8081; | ||||
|         proxy_http_version 1.1; | ||||
|         proxy_set_header Upgrade $http_upgrade; | ||||
|         proxy_set_header Connection "Upgrade"; | ||||
|         proxy_set_header Host $host; | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										42
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										42
									
								
								package.json
									
									
									
									
									
								
							| @@ -12,21 +12,20 @@ | ||||
|     "@fortawesome/free-regular-svg-icons": "^5.15.1", | ||||
|     "@fortawesome/free-solid-svg-icons": "^5.15.1", | ||||
|     "@fortawesome/react-fontawesome": "^0.1.13", | ||||
|     "bootstrap": "^5.0.2", | ||||
|     "bootstrap": "^4.5.3", | ||||
|     "plyr-react": "^3.0.7", | ||||
|     "react": "^17.0.1", | ||||
|     "react-bootstrap": "^1.4.0", | ||||
|     "react-dom": "^17.0.1", | ||||
|     "react-router": "^5.2.0", | ||||
|     "react-router-dom": "^5.2.0", | ||||
|     "typescript": "^4.3.5" | ||||
|     "typescript": "^4.1.3" | ||||
|   }, | ||||
|   "scripts": { | ||||
|     "start": "react-scripts start", | ||||
|     "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 --single -i apiGo/ -o doc/" | ||||
|     "lint": "eslint --format gitlab src/" | ||||
|   }, | ||||
|   "jest": { | ||||
|     "collectCoverageFrom": [ | ||||
| @@ -53,37 +52,30 @@ | ||||
|     ] | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@testing-library/jest-dom": "^5.14.1", | ||||
|     "@testing-library/react": "^12.0.0", | ||||
|     "@testing-library/user-event": "^13.2.1", | ||||
|     "@types/jest": "^26.0.24", | ||||
|     "@types/node": "^16.4.7", | ||||
|     "@types/react": "^17.0.15", | ||||
|     "@types/react-dom": "^17.0.9", | ||||
|     "@types/react-router": "5.1.16", | ||||
|     "@types/react-router-dom": "^5.1.8", | ||||
|     "@typescript-eslint/eslint-plugin": "^4.28.5", | ||||
|     "@typescript-eslint/parser": "^4.28.5", | ||||
|     "apidoc": "^0.28.1", | ||||
|     "@testing-library/jest-dom": "^5.11.6", | ||||
|     "@testing-library/react": "^11.2.2", | ||||
|     "@testing-library/user-event": "^12.6.0", | ||||
|     "@types/jest": "^26.0.19", | ||||
|     "@types/node": "^14.14.31", | ||||
|     "@types/react": "^17.0.2", | ||||
|     "@types/react-dom": "^17.0.1", | ||||
|     "@types/react-router": "5.1.12", | ||||
|     "@types/react-router-dom": "^5.1.6", | ||||
|     "@typescript-eslint/eslint-plugin": "^4.17.0", | ||||
|     "@typescript-eslint/parser": "^4.17.0", | ||||
|     "enzyme": "^3.11.0", | ||||
|     "enzyme-adapter-react-16": "^1.15.5", | ||||
|     "eslint": "^7.31.0", | ||||
|     "eslint": "^7.22.0", | ||||
|     "eslint-config-prettier": "^8.1.0", | ||||
|     "eslint-formatter-gitlab": "^2.2.0", | ||||
|     "eslint-plugin-eslint-comments": "^3.2.0", | ||||
|     "eslint-plugin-jest": "^24.4.0", | ||||
|     "eslint-plugin-jest": "^24.3.1", | ||||
|     "eslint-plugin-prettier": "^3.3.1", | ||||
|     "eslint-plugin-react": "^7.22.0", | ||||
|     "eslint-plugin-react-hooks": "^4.2.0", | ||||
|     "jest-junit": "^12.0.0", | ||||
|     "prettier": "^2.3.2", | ||||
|     "prettier": "^2.2.1", | ||||
|     "prettier-config": "^1.0.0", | ||||
|     "react-scripts": "4.0.3" | ||||
|   }, | ||||
|   "apidoc": { | ||||
|     "name": "OpenMediaCenter", | ||||
|     "description": "API Documentation of OpenMediaCenter", | ||||
|     "title": "OpenMediaCenter Doc", | ||||
|     "sampleUrl": null | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								public/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 3.1 KiB | 
| @@ -2,14 +2,14 @@ | ||||
| <html lang="en"> | ||||
|   <head> | ||||
|     <meta charset="utf-8" /> | ||||
|     <link rel="icon" href="%PUBLIC_URL%/logo_circle.png" /> | ||||
|     <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||||
|     <meta name="theme-color" content="#000000" /> | ||||
|     <meta | ||||
|       name="description" | ||||
|       content="A Application to run a Mediacenter in your local network" | ||||
|     /> | ||||
|     <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo_circle.png" /> | ||||
|     <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> | ||||
|     <!-- | ||||
|       manifest.json provides metadata used when your web app is installed on a | ||||
|       user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								public/logo192.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/logo192.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 5.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/logo512.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/logo512.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 9.4 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 25 KiB | 
| @@ -3,9 +3,19 @@ | ||||
|   "name": "Create React App Sample", | ||||
|   "icons": [ | ||||
|     { | ||||
|       "src": "logo_circle.png", | ||||
|       "src": "favicon.ico", | ||||
|       "sizes": "64x64 32x32 24x24 16x16", | ||||
|       "type": "image/x-icon" | ||||
|     }, | ||||
|     { | ||||
|       "src": "logo192.png", | ||||
|       "type": "image/png", | ||||
|       "sizes": "192x192" | ||||
|     }, | ||||
|     { | ||||
|       "src": "logo512.png", | ||||
|       "type": "image/png", | ||||
|       "sizes": "512x512" | ||||
|     } | ||||
|   ], | ||||
|   "start_url": ".", | ||||
|   | ||||
| @@ -12,7 +12,6 @@ | ||||
|     margin-left: 20px; | ||||
|     opacity: 0.6; | ||||
|     text-transform: capitalize; | ||||
|     text-decoration: none; | ||||
| } | ||||
|  | ||||
| .navitem:hover { | ||||
|   | ||||
| @@ -2,7 +2,6 @@ import React from 'react'; | ||||
| import App from './App'; | ||||
| import {shallow} from 'enzyme'; | ||||
| import GlobalInfos from "./utils/GlobalInfos"; | ||||
| import {LoginContext} from './utils/context/LoginContext'; | ||||
|  | ||||
| describe('<App/>', function () { | ||||
|     it('renders without crashing ', function () { | ||||
| @@ -19,7 +18,24 @@ describe('<App/>', function () { | ||||
|     it('are navlinks correct', function () { | ||||
|         const wrapper = shallow(<App/>); | ||||
|         wrapper.setState({password: false}); | ||||
|         expect(wrapper.find('.navitem')).toHaveLength(5); | ||||
|         expect(wrapper.find('.navitem')).toHaveLength(4); | ||||
|     }); | ||||
|  | ||||
|     it('test initial fetch from api', done => { | ||||
|         callAPIMock({ | ||||
|             MediacenterName: 'testname' | ||||
|         }) | ||||
|  | ||||
|         GlobalInfos.enableDarkTheme = jest.fn((r) => {}) | ||||
|  | ||||
|         const wrapper = shallow(<App/>); | ||||
|  | ||||
|         process.nextTick(() => { | ||||
|             expect(document.title).toBe('testname'); | ||||
|  | ||||
|             global.fetch.mockClear(); | ||||
|             done(); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     it('test render of password page', function () { | ||||
|   | ||||
							
								
								
									
										244
									
								
								src/App.tsx
									
									
									
									
									
								
							
							
						
						
									
										244
									
								
								src/App.tsx
									
									
									
									
									
								
							| @@ -1,4 +1,4 @@ | ||||
| import React, {useContext} from 'react'; | ||||
| import React from 'react'; | ||||
| import HomePage from './pages/HomePage/HomePage'; | ||||
| import RandomPage from './pages/RandomPage/RandomPage'; | ||||
| import GlobalInfos from './utils/GlobalInfos'; | ||||
| @@ -9,18 +9,17 @@ import style from './App.module.css'; | ||||
|  | ||||
| import SettingsPage from './pages/SettingsPage/SettingsPage'; | ||||
| import CategoryPage from './pages/CategoryPage/CategoryPage'; | ||||
| import {APINode, apiTokenValid, callApiUnsafe, refreshAPIToken} from './utils/Api'; | ||||
|  | ||||
| import {NavLink, Route, Switch, useRouteMatch} from 'react-router-dom'; | ||||
| import {BrowserRouter as Router, NavLink, Route, Switch} from 'react-router-dom'; | ||||
| import Player from './pages/Player/Player'; | ||||
| import ActorOverviewPage from './pages/ActorOverviewPage/ActorOverviewPage'; | ||||
| import ActorPage from './pages/ActorPage/ActorPage'; | ||||
| import {SettingsTypes} from './types/ApiTypes'; | ||||
| import AuthenticationPage from './pages/AuthenticationPage/AuthenticationPage'; | ||||
| import TVShowPage from './pages/TVShowPage/TVShowPage'; | ||||
| import TVPlayer from './pages/TVShowPage/TVPlayer'; | ||||
| import {LoginContextProvider} from './utils/context/LoginContextProvider'; | ||||
| import {FeatureContext} from './utils/context/FeatureContext'; | ||||
|  | ||||
| interface state { | ||||
|     password: boolean | null; // null if uninitialized - true if pwd needed false if not needed | ||||
|     mediacentername: string; | ||||
| } | ||||
|  | ||||
| @@ -31,132 +30,147 @@ class App extends React.Component<{}, state> { | ||||
|     constructor(props: {}) { | ||||
|         super(props); | ||||
|  | ||||
|         let pwdneeded: boolean | null = null; | ||||
|  | ||||
|         if (apiTokenValid()) { | ||||
|             pwdneeded = false; | ||||
|         } else { | ||||
|             refreshAPIToken((err) => { | ||||
|                 if (err === 'invalid_client') { | ||||
|                     this.setState({password: true}); | ||||
|                 } else if (err === '') { | ||||
|                     this.setState({password: false}); | ||||
|                 } else { | ||||
|                     console.log('unimplemented token error: ' + err); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         this.state = { | ||||
|             mediacentername: 'OpenMediaCenter' | ||||
|             mediacentername: 'OpenMediaCenter', | ||||
|             password: pwdneeded | ||||
|         }; | ||||
|  | ||||
|         // force an update on theme change | ||||
|         GlobalInfos.onThemeChange(() => { | ||||
|             this.forceUpdate(); | ||||
|         }); | ||||
|  | ||||
|         // set the hook to load passwordfield on global func call | ||||
|         GlobalInfos.loadPasswordPage = (callback?: () => void): void => { | ||||
|             // try refreshing the token | ||||
|             refreshAPIToken((err) => { | ||||
|                 if (err !== '') { | ||||
|                     this.setState({password: true}); | ||||
|                 } else { | ||||
|                     // call callback if request was successful | ||||
|                     if (callback) { | ||||
|                         callback(); | ||||
|                     } | ||||
|                 } | ||||
|             }, true); | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     initialAPICall(): void { | ||||
|         // this is the first api call so if it fails we know there is no connection to backend | ||||
|         callApiUnsafe(APINode.Init, {action: 'loadInitialData'}, (result: SettingsTypes.initialApiCallData) => { | ||||
|             // set theme | ||||
|             GlobalInfos.enableDarkTheme(result.DarkMode); | ||||
|  | ||||
|             GlobalInfos.setVideoPath(result.VideoPath); | ||||
|  | ||||
|             this.setState({ | ||||
|                 mediacentername: result.MediacenterName | ||||
|             }); | ||||
|             // set tab title to received mediacenter name | ||||
|             document.title = result.MediacenterName; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     componentDidMount(): void { | ||||
|         this.initialAPICall(); | ||||
|     } | ||||
|  | ||||
|     render(): JSX.Element { | ||||
|         const themeStyle = GlobalInfos.getThemeStyle(); | ||||
|         // add the main theme to the page body | ||||
|         document.body.className = GlobalInfos.getThemeStyle().backgroundcolor; | ||||
|         document.body.className = themeStyle.backgroundcolor; | ||||
|  | ||||
|         return ( | ||||
|             <LoginContextProvider> | ||||
|                 <Switch> | ||||
|                     <Route path='/login'> | ||||
|                         <AuthenticationPage /> | ||||
|                     </Route> | ||||
|                     <Route path='/media'> | ||||
|                         {this.navBar()} | ||||
|                         <MyRouter /> | ||||
|                     </Route> | ||||
|                 </Switch> | ||||
|             </LoginContextProvider> | ||||
|         ); | ||||
|         if (this.state.password === true) { | ||||
|             // render authentication page if auth is neccessary | ||||
|             return <AuthenticationPage onSuccessLogin={(): void => this.setState({password: false})} />; | ||||
|         } else if (this.state.password === false) { | ||||
|             return ( | ||||
|                 <Router> | ||||
|                     <div className={style.app}> | ||||
|                         <div | ||||
|                             className={[style.navcontainer, themeStyle.backgroundcolor, themeStyle.textcolor, themeStyle.hrcolor].join( | ||||
|                                 ' ' | ||||
|                             )}> | ||||
|                             <div className={style.navbrand}>{this.state.mediacentername}</div> | ||||
|                             <NavLink | ||||
|                                 className={[style.navitem, themeStyle.navitem].join(' ')} | ||||
|                                 to={'/'} | ||||
|                                 activeStyle={{opacity: '0.85'}}> | ||||
|                                 Home | ||||
|                             </NavLink> | ||||
|                             <NavLink | ||||
|                                 className={[style.navitem, themeStyle.navitem].join(' ')} | ||||
|                                 to={'/random'} | ||||
|                                 activeStyle={{opacity: '0.85'}}> | ||||
|                                 Random Video | ||||
|                             </NavLink> | ||||
|  | ||||
|                             <NavLink | ||||
|                                 className={[style.navitem, themeStyle.navitem].join(' ')} | ||||
|                                 to={'/categories'} | ||||
|                                 activeStyle={{opacity: '0.85'}}> | ||||
|                                 Categories | ||||
|                             </NavLink> | ||||
|                             <NavLink | ||||
|                                 className={[style.navitem, themeStyle.navitem].join(' ')} | ||||
|                                 to={'/settings'} | ||||
|                                 activeStyle={{opacity: '0.85'}}> | ||||
|                                 Settings | ||||
|                             </NavLink> | ||||
|                         </div> | ||||
|                         {this.routing()} | ||||
|                     </div> | ||||
|                 </Router> | ||||
|             ); | ||||
|         } else { | ||||
|             return <>still loading...</>; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     static contextType = FeatureContext; | ||||
|  | ||||
|     /** | ||||
|      * render the top navigation bar | ||||
|      */ | ||||
|     navBar(): JSX.Element { | ||||
|         const themeStyle = GlobalInfos.getThemeStyle(); | ||||
|  | ||||
|     routing(): JSX.Element { | ||||
|         return ( | ||||
|             <div className={[style.navcontainer, themeStyle.backgroundcolor, themeStyle.textcolor, themeStyle.hrcolor].join(' ')}> | ||||
|                 <div className={style.navbrand}>{this.state.mediacentername}</div> | ||||
|                 <NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/media'} activeStyle={{opacity: '0.85'}}> | ||||
|                     Home | ||||
|                 </NavLink> | ||||
|                 <NavLink | ||||
|                     className={[style.navitem, themeStyle.navitem].join(' ')} | ||||
|                     to={'/media/random'} | ||||
|                     activeStyle={{opacity: '0.85'}}> | ||||
|                     Random Video | ||||
|                 </NavLink> | ||||
|  | ||||
|                 <NavLink | ||||
|                     className={[style.navitem, themeStyle.navitem].join(' ')} | ||||
|                     to={'/media/categories'} | ||||
|                     activeStyle={{opacity: '0.85'}}> | ||||
|                     Categories | ||||
|                 </NavLink> | ||||
|  | ||||
|                 <NavLink | ||||
|                     className={[style.navitem, themeStyle.navitem].join(' ')} | ||||
|                     to={'/media/actors'} | ||||
|                     activeStyle={{opacity: '0.85'}}> | ||||
|                     Actors | ||||
|                 </NavLink> | ||||
|  | ||||
|                 {this.context.TVShowEnabled ? ( | ||||
|                     <NavLink | ||||
|                         className={[style.navitem, themeStyle.navitem].join(' ')} | ||||
|                         to={'/media/tvshows'} | ||||
|                         activeStyle={{opacity: '0.85'}}> | ||||
|                         TV Shows | ||||
|                     </NavLink> | ||||
|                 ) : null} | ||||
|  | ||||
|                 <NavLink | ||||
|                     className={[style.navitem, themeStyle.navitem].join(' ')} | ||||
|                     to={'/media/settings'} | ||||
|                     activeStyle={{opacity: '0.85'}}> | ||||
|                     Settings | ||||
|                 </NavLink> | ||||
|             </div> | ||||
|             <Switch> | ||||
|                 <Route path='/random'> | ||||
|                     <RandomPage /> | ||||
|                 </Route> | ||||
|                 <Route path='/categories'> | ||||
|                     <CategoryPage /> | ||||
|                 </Route> | ||||
|                 <Route path='/settings'> | ||||
|                     <SettingsPage /> | ||||
|                 </Route> | ||||
|                 <Route exact path='/player/:id'> | ||||
|                     <Player /> | ||||
|                 </Route> | ||||
|                 <Route exact path='/actors'> | ||||
|                     <ActorOverviewPage /> | ||||
|                 </Route> | ||||
|                 <Route path='/actors/:id'> | ||||
|                     <ActorPage /> | ||||
|                 </Route> | ||||
|                 <Route path='/'> | ||||
|                     <HomePage /> | ||||
|                 </Route> | ||||
|             </Switch> | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  | ||||
| const MyRouter = (): JSX.Element => { | ||||
|     const match = useRouteMatch(); | ||||
|     const features = useContext(FeatureContext); | ||||
|  | ||||
|     return ( | ||||
|         <Switch> | ||||
|             <Route exact path={`${match.url}/random`}> | ||||
|                 <RandomPage /> | ||||
|             </Route> | ||||
|             <Route path={`${match.url}/categories`}> | ||||
|                 <CategoryPage /> | ||||
|             </Route> | ||||
|             <Route path={`${match.url}/settings`}> | ||||
|                 <SettingsPage /> | ||||
|             </Route> | ||||
|             <Route exact path={`${match.url}/player/:id`}> | ||||
|                 <Player /> | ||||
|             </Route> | ||||
|             <Route exact path={`${match.url}/actors`}> | ||||
|                 <ActorOverviewPage /> | ||||
|             </Route> | ||||
|             <Route exact path={`${match.url}/actors/:id`}> | ||||
|                 <ActorPage /> | ||||
|             </Route> | ||||
|  | ||||
|             {features.TVShowEnabled ? ( | ||||
|                 <Route path={`${match.url}/tvshows`}> | ||||
|                     <TVShowPage /> | ||||
|                 </Route> | ||||
|             ) : null} | ||||
|  | ||||
|             {features.TVShowEnabled ? ( | ||||
|                 <Route exact path={`${match.url}/tvplayer/:id`}> | ||||
|                     <TVPlayer /> | ||||
|                 </Route> | ||||
|             ) : null} | ||||
|  | ||||
|             <Route path={`${match.url}/`}> | ||||
|                 <HomePage /> | ||||
|             </Route> | ||||
|         </Switch> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| export default App; | ||||
|   | ||||
| @@ -21,7 +21,7 @@ class ActorTile extends React.Component<Props> { | ||||
|         if (this.props.onClick) { | ||||
|             return this.renderActorTile(this.props.onClick); | ||||
|         } else { | ||||
|             return <Link to={{pathname: '/media/actors/' + this.props.actor.ActorId}}>{this.renderActorTile(() => {})}</Link>; | ||||
|             return <Link to={{pathname: '/actors/' + this.props.actor.ActorId}}>{this.renderActorTile(() => {})}</Link>; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,30 +0,0 @@ | ||||
| .dropArea { | ||||
|     border: 2px dashed #ccc; | ||||
|     border-radius: 20px; | ||||
|     width: 480px; | ||||
|     font-family: sans-serif; | ||||
|     padding: 20px; | ||||
| } | ||||
|  | ||||
| .dropArea:hover { | ||||
|     cursor: pointer; | ||||
| } | ||||
|  | ||||
| .dropArea.highlight { | ||||
|     border-color: purple; | ||||
| } | ||||
|  | ||||
| .myForm { | ||||
|     margin-bottom: 10px; | ||||
| } | ||||
|  | ||||
| .progresswrapper { | ||||
|     width: 100%; | ||||
|     height: 5px; | ||||
|     margin-top: 3px; | ||||
| } | ||||
|  | ||||
| .finished { | ||||
|     margin-top: 10px; | ||||
|     text-align: center; | ||||
| } | ||||
| @@ -1,108 +0,0 @@ | ||||
| import style from './DropZone.module.css'; | ||||
| import React, {useState} from 'react'; | ||||
| import {cookie} from '../../utils/context/Cookie'; | ||||
| import GlobalInfos from '../../utils/GlobalInfos'; | ||||
|  | ||||
| export const DropZone = (): JSX.Element => { | ||||
|     const [ondrag, setDrag] = useState(0); | ||||
|     const [percent, setpercent] = useState(0.0); | ||||
|     const [finished, setfinished] = useState<string | null>(null); | ||||
|  | ||||
|     const theme = GlobalInfos.getThemeStyle(); | ||||
|  | ||||
|     const uploadFile = (f: FileList): void => { | ||||
|         const xhr = new XMLHttpRequest(); // create XMLHttpRequest | ||||
|         const data = new FormData(); // create formData object | ||||
|  | ||||
|         for (let i = 0; i < f.length; i++) { | ||||
|             const file = f.item(i); | ||||
|             if (file) { | ||||
|                 data.append('file' + i, file); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         xhr.onload = function (): void { | ||||
|             console.log(this.responseText); // whatever the server returns | ||||
|  | ||||
|             const resp = JSON.parse(this.responseText); | ||||
|             if (resp.Message === 'finished all files') { | ||||
|                 setfinished(''); | ||||
|             } else { | ||||
|                 setfinished(resp.Message); | ||||
|             } | ||||
|  | ||||
|             setTimeout(() => { | ||||
|                 setpercent(0); | ||||
|                 setfinished(null); | ||||
|             }, 2000); | ||||
|         }; | ||||
|  | ||||
|         xhr.upload.onprogress = function (e): void { | ||||
|             console.log(e.loaded / e.total); | ||||
|             setpercent((e.loaded * 100.0) / e.total); | ||||
|         }; | ||||
|  | ||||
|         xhr.open('post', '/api/video/fileupload'); // open connection | ||||
|         xhr.setRequestHeader('Accept', 'multipart/form-data'); | ||||
|  | ||||
|         const tkn = cookie.Load(); | ||||
|         if (tkn) { | ||||
|             xhr.setRequestHeader('Token', tkn.Token); | ||||
|         } | ||||
|  | ||||
|         xhr.send(data); // send data | ||||
|     }; | ||||
|  | ||||
|     return ( | ||||
|         <div | ||||
|             className={style.dropArea + (ondrag > 0 ? ' ' + style.highlight : '') + ' ' + theme.secbackground} | ||||
|             onDragEnter={(e): void => { | ||||
|                 e.preventDefault(); | ||||
|                 e.stopPropagation(); | ||||
|                 setDrag(ondrag + 1); | ||||
|             }} | ||||
|             onDragLeave={(e): void => { | ||||
|                 e.preventDefault(); | ||||
|                 e.stopPropagation(); | ||||
|                 setDrag(ondrag - 1); | ||||
|             }} | ||||
|             onDragOver={(e): void => { | ||||
|                 e.stopPropagation(); | ||||
|                 e.preventDefault(); | ||||
|             }} | ||||
|             onDrop={(e): void => { | ||||
|                 setDrag(0); | ||||
|                 e.preventDefault(); | ||||
|                 e.stopPropagation(); | ||||
|  | ||||
|                 uploadFile(e.dataTransfer.files); | ||||
|             }} | ||||
|             onClick={(): void => { | ||||
|                 let input = document.createElement('input'); | ||||
|                 input.type = 'file'; | ||||
|                 input.multiple = true; | ||||
|                 input.onchange = function (): void { | ||||
|                     if (input.files) { | ||||
|                         uploadFile(input.files); | ||||
|                     } | ||||
|                 }; | ||||
|                 input.click(); | ||||
|             }}> | ||||
|             <div className={style.myForm}> | ||||
|                 <p>To upload new Videos darg and drop them here or click to select some...</p> | ||||
|                 <div className={style.progresswrapper}> | ||||
|                     <div style={{width: percent + '%', backgroundColor: 'green', height: 5}} /> | ||||
|                 </div> | ||||
|                 {finished !== null ? ( | ||||
|                     finished === '' ? ( | ||||
|                         <div className={style.finished}>Finished uploading</div> | ||||
|                     ) : ( | ||||
|                         <div className={style.finished}>Upload failed: {finished}</div> | ||||
|                     ) | ||||
|                 ) : ( | ||||
|                     <></> | ||||
|                 )} | ||||
|             </div> | ||||
|         </div> | ||||
|     ); | ||||
| }; | ||||
| @@ -1,49 +0,0 @@ | ||||
| import {shallow} from 'enzyme'; | ||||
| import React from 'react'; | ||||
| import DynamicContentContainer from './DynamicContentContainer'; | ||||
|  | ||||
| describe('<DynamicContentContainer/>', function () { | ||||
|     it('renders without crashing ', function () { | ||||
|         const wrapper = shallow(<DynamicContentContainer data={[]} renderElement={(el) => (<></>)}/>); | ||||
|         wrapper.unmount(); | ||||
|     }); | ||||
|  | ||||
|     it('inserts tiles correctly if enough available', () => { | ||||
|         const wrapper = shallow(<DynamicContentContainer data={[ | ||||
|             {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {} | ||||
|         ]} renderElement={(el) => (<a/>)}/>); | ||||
|         expect(wrapper.find('a')).toHaveLength(16); | ||||
|     }); | ||||
|  | ||||
|     it('inserts tiles correctly if not enough available', () => { | ||||
|         const wrapper = shallow(<DynamicContentContainer data={[ | ||||
|             {}, {}, {}, {} | ||||
|         ]} renderElement={(el) => (<a/>)}/>); | ||||
|         expect(wrapper.find('a')).toHaveLength(4); | ||||
|     }); | ||||
|  | ||||
|     it('no items available', () => { | ||||
|         const wrapper = shallow(<DynamicContentContainer data={[]} renderElement={(el) => (<a/>)}/>); | ||||
|         expect(wrapper.find('a')).toHaveLength(0); | ||||
|         expect(wrapper.find('.maincontent').text()).toBe('no items to show!'); | ||||
|     }); | ||||
|  | ||||
|     it('test clean', function () { | ||||
|         const wrapper = shallow(<DynamicContentContainer data={[{}, {}, {}]} renderElement={(el) => (<a/>)}/>); | ||||
|         expect(wrapper.find('a')).toHaveLength(3); | ||||
|         wrapper.instance().clean(); | ||||
|         expect(wrapper.find('a')).toHaveLength(0); | ||||
|     }); | ||||
|  | ||||
|     it('test update', function () { | ||||
|         const wrapper = shallow(<DynamicContentContainer data={[{}, {}, {}]} renderElement={(el) => (<a/>)}/>); | ||||
|  | ||||
|         const func = jest.fn(); | ||||
|         wrapper.instance().clean = func; | ||||
|  | ||||
|         // perform component update | ||||
|         wrapper.setProps({data: [{}, {}]}); | ||||
|  | ||||
|         expect(func).toHaveBeenCalledTimes(1); | ||||
|     }); | ||||
| }); | ||||
| @@ -1,113 +0,0 @@ | ||||
| import React from 'react'; | ||||
| import style from './DynamicContentContainer.module.css'; | ||||
|  | ||||
| interface Props<T> { | ||||
|     renderElement: (elem: T) => JSX.Element; | ||||
|     data: T[]; | ||||
|     initialLoadNr?: number; | ||||
| } | ||||
|  | ||||
| interface state<T> { | ||||
|     loadeditems: T[]; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * A videocontainer storing lots of Preview elements | ||||
|  * includes scroll handling and loading of preview infos | ||||
|  */ | ||||
| class DynamicContentContainer<T> extends React.Component<Props<T>, state<T>> { | ||||
|     // stores current index of loaded elements | ||||
|     loadindex: number = 0; | ||||
|  | ||||
|     readonly InitialLoadNR = this.props.initialLoadNr | ||||
|         ? this.props.initialLoadNr === -1 | ||||
|             ? this.props.data.length | ||||
|             : this.props.initialLoadNr | ||||
|         : 16; | ||||
|  | ||||
|     constructor(props: Props<T>) { | ||||
|         super(props); | ||||
|  | ||||
|         this.state = { | ||||
|             loadeditems: [] | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     componentDidMount(): void { | ||||
|         document.addEventListener('scroll', this.trackScrolling); | ||||
|  | ||||
|         this.loadPreviewBlock(this.InitialLoadNR); | ||||
|     } | ||||
|  | ||||
|     componentDidUpdate(prevProps: Props<T>): void { | ||||
|         // when source props change force update! | ||||
|         if ( | ||||
|             // diff the two arrays | ||||
|             this.props.data | ||||
|                 .filter((x) => !prevProps.data.includes(x)) | ||||
|                 .concat(prevProps.data.filter((x) => !this.props.data.includes(x))).length !== 0 | ||||
|         ) { | ||||
|             this.clean((): void => { | ||||
|                 this.loadPreviewBlock(this.InitialLoadNR); | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * clear all elements rendered... | ||||
|      */ | ||||
|     clean(callback: () => void): void { | ||||
|         this.loadindex = 0; | ||||
|         this.setState({loadeditems: []}, callback); | ||||
|     } | ||||
|  | ||||
|     render(): JSX.Element { | ||||
|         return ( | ||||
|             <div className={style.maincontent}> | ||||
|                 {this.state.loadeditems.map((elem) => { | ||||
|                     return this.props.renderElement(elem); | ||||
|                 })} | ||||
|                 {/*todo css for no items to show*/} | ||||
|                 {this.state.loadeditems.length === 0 ? 'no items to show!' : null} | ||||
|                 {this.props.children} | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     componentWillUnmount(): void { | ||||
|         // unbind scroll listener when unmounting component | ||||
|         document.removeEventListener('scroll', this.trackScrolling); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * load previews to the container | ||||
|      * @param nr number of previews to load | ||||
|      */ | ||||
|     loadPreviewBlock(nr: number): void { | ||||
|         let ret = []; | ||||
|         for (let i = 0; i < nr; i++) { | ||||
|             // only add if not end | ||||
|             if (this.props.data.length > this.loadindex + i) { | ||||
|                 ret.push(this.props.data[this.loadindex + i]); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         this.setState({ | ||||
|             loadeditems: [...this.state.loadeditems, ...ret] | ||||
|         }); | ||||
|  | ||||
|         this.loadindex += nr; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * scroll event handler -> load new previews if on bottom | ||||
|      */ | ||||
|     trackScrolling = (): void => { | ||||
|         // comparison if current scroll position is on bottom --> 200 is bottom offset to trigger load | ||||
|         if (window.innerHeight + document.documentElement.scrollTop + 200 >= document.documentElement.offsetHeight) { | ||||
|             this.loadPreviewBlock(8); | ||||
|         } | ||||
|     }; | ||||
| } | ||||
|  | ||||
| export default DynamicContentContainer; | ||||
| @@ -2,10 +2,7 @@ | ||||
|     background-color: green; | ||||
|     border-radius: 5px; | ||||
|     border-width: 0; | ||||
|     color: white; | ||||
|     margin-right: 15px; | ||||
|     padding: 6px; | ||||
| } | ||||
|  | ||||
| .button:hover{ | ||||
|     opacity: 0.7; | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,14 @@ | ||||
| import {shallow} from 'enzyme'; | ||||
| import React from 'react'; | ||||
| import {Button, IconButton} from './Button'; | ||||
| import {Button} from './Button'; | ||||
|  | ||||
| function prepareFetchApi(response) { | ||||
|     const mockJsonPromise = Promise.resolve(response); | ||||
|     const mockFetchPromise = Promise.resolve({ | ||||
|         json: () => mockJsonPromise | ||||
|     }); | ||||
|     return (jest.fn().mockImplementation(() => mockFetchPromise)); | ||||
| } | ||||
|  | ||||
| describe('<Button/>', function () { | ||||
|     it('renders without crashing ', function () { | ||||
| @@ -22,24 +30,3 @@ describe('<Button/>', function () { | ||||
|         expect(func).toHaveBeenCalledTimes(1); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| describe('<IconButton/>', function () { | ||||
|     it('renders without crashing ', function () { | ||||
|         const wrapper = shallow(<IconButton onClick={() => {}} title='test'/>); | ||||
|         wrapper.unmount(); | ||||
|     }); | ||||
|  | ||||
|     it('renders title', function () { | ||||
|         const wrapper = shallow(<IconButton onClick={() => {}} title='test1'/>); | ||||
|         expect(wrapper.text()).toBe('<FontAwesomeIcon />test1'); | ||||
|     }); | ||||
|  | ||||
|     it('test onclick handling', () => { | ||||
|         const func = jest.fn(); | ||||
|         const wrapper = shallow(<IconButton onClick={func} title='test1'/>); | ||||
|  | ||||
|         wrapper.find('button').simulate('click'); | ||||
|  | ||||
|         expect(func).toHaveBeenCalledTimes(1); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -1,8 +1,5 @@ | ||||
| import React from 'react'; | ||||
| import style from './Button.module.css'; | ||||
| import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; | ||||
| import {IconDefinition} from '@fortawesome/fontawesome-common-types'; | ||||
| import GlobalInfos from '../../utils/GlobalInfos'; | ||||
|  | ||||
| interface ButtonProps { | ||||
|     title: string | JSX.Element; | ||||
| @@ -11,30 +8,9 @@ interface ButtonProps { | ||||
| } | ||||
|  | ||||
| export function Button(props: ButtonProps): JSX.Element { | ||||
|     const theme = GlobalInfos.getThemeStyle(); | ||||
|  | ||||
|     return ( | ||||
|         <button className={style.button + ' ' + theme.textcolor} style={props.color} onClick={props.onClick}> | ||||
|         <button className={style.button} style={props.color} onClick={props.onClick}> | ||||
|             {props.title} | ||||
|         </button> | ||||
|     ); | ||||
| } | ||||
|  | ||||
| interface IconButtonProps { | ||||
|     title: string | JSX.Element; | ||||
|     onClick?: () => void; | ||||
|     icon: IconDefinition; | ||||
| } | ||||
|  | ||||
| export function IconButton(props: IconButtonProps): JSX.Element { | ||||
|     const theme = GlobalInfos.getThemeStyle(); | ||||
|  | ||||
|     return ( | ||||
|         <button className={style.button + ' ' + theme.textcolor} style={{backgroundColor: '#00000000'}} onClick={props.onClick}> | ||||
|             <span style={{fontSize: 12}}> | ||||
|                 <FontAwesomeIcon className={theme.textcolor} icon={props.icon} size='2x' /> | ||||
|             </span> | ||||
|             <span style={{marginLeft: 10}}>{props.title}</span> | ||||
|         </button> | ||||
|     ); | ||||
| } | ||||
|   | ||||
| @@ -1,51 +0,0 @@ | ||||
| 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); | ||||
|     }); | ||||
| }); | ||||
| @@ -1,58 +0,0 @@ | ||||
| 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> | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
| @@ -25,7 +25,7 @@ describe('<NewActorPopupContent/>', () => { | ||||
|         const wrapper = shallow(<NewActorPopupContent onHide={() => {func();}}/>); | ||||
|  | ||||
|         // manually set typed in actorname | ||||
|         wrapper.instance().nameValue = 'testactorname'; | ||||
|         wrapper.instance().value = 'testactorname'; | ||||
|  | ||||
|         global.fetch = prepareFetchApi({}); | ||||
|  | ||||
| @@ -55,6 +55,6 @@ describe('<NewActorPopupContent/>', () => { | ||||
|  | ||||
|         wrapper.find('input').simulate('change', {target: {value: 'testinput'}}); | ||||
|  | ||||
|         expect(wrapper.instance().nameValue).toBe('testinput'); | ||||
|         expect(wrapper.instance().value).toBe('testinput'); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -22,7 +22,7 @@ class NewActorPopup extends React.Component<NewActorPopupProps> { | ||||
| } | ||||
|  | ||||
| export class NewActorPopupContent extends React.Component<NewActorPopupProps> { | ||||
|     nameValue: string | undefined; | ||||
|     value: string | undefined; | ||||
|  | ||||
|     render(): JSX.Element { | ||||
|         return ( | ||||
| @@ -32,7 +32,7 @@ export class NewActorPopupContent extends React.Component<NewActorPopupProps> { | ||||
|                         type='text' | ||||
|                         placeholder='Actor Name' | ||||
|                         onChange={(v): void => { | ||||
|                             this.nameValue = v.target.value; | ||||
|                             this.value = v.target.value; | ||||
|                         }} | ||||
|                     /> | ||||
|                 </div> | ||||
| @@ -48,11 +48,11 @@ export class NewActorPopupContent extends React.Component<NewActorPopupProps> { | ||||
|      */ | ||||
|     storeselection(): void { | ||||
|         // check if user typed in name | ||||
|         if (this.nameValue === '' || this.nameValue === undefined) { | ||||
|         if (this.value === '' || this.value === undefined) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         callAPI(APINode.Actor, {action: 'createActor', ActorName: this.nameValue}, (result: GeneralSuccess) => { | ||||
|         callAPI(APINode.Actor, {action: 'createActor', actorname: this.value}, (result: GeneralSuccess) => { | ||||
|             if (result.result !== 'success') { | ||||
|                 console.log('error occured while writing to db -- todo error handling'); | ||||
|                 console.log(result.result); | ||||
|   | ||||
| @@ -1,8 +1,19 @@ | ||||
| .previewtitle { | ||||
|     font-size: smaller; | ||||
| .previewText { | ||||
|     font-size: 15px; | ||||
|     font-weight: bold; | ||||
|     height: 20px; | ||||
|     height: 42px; | ||||
|     width: 100%; | ||||
|     text-align: center; | ||||
|     position: absolute; | ||||
|     bottom: 0; | ||||
|     background-color: rgba(60, 61, 72, 0.7); | ||||
|     color: white; | ||||
|     border-bottom-left-radius: 20px; | ||||
|     border-bottom-right-radius: 20px; | ||||
|  | ||||
|     border-top-width: 1px; | ||||
|     border-top-color: #3c3d48; | ||||
|     border-top-style: solid; | ||||
| } | ||||
|  | ||||
| .previewpic { | ||||
| @@ -11,6 +22,8 @@ | ||||
|     min-width: 266px; | ||||
|     overflow: hidden; | ||||
|     text-align: center; | ||||
|  | ||||
|     border-radius: 20px; | ||||
| } | ||||
|  | ||||
| .loadAnimation { | ||||
| @@ -19,6 +32,13 @@ | ||||
|     vertical-align: middle; | ||||
| } | ||||
|  | ||||
| .previewimage { | ||||
|     max-height: 400px; | ||||
|     max-width: 410px; | ||||
|     min-height: 150px; | ||||
|     min-width: 266px; | ||||
| } | ||||
|  | ||||
| .previewbottom { | ||||
|     height: 20px; | ||||
| } | ||||
| @@ -30,6 +50,7 @@ | ||||
|     margin-left: 25px; | ||||
|     margin-top: 25px; | ||||
|     opacity: 0.85; | ||||
|     position: relative; | ||||
| } | ||||
|  | ||||
| .videopreview:hover { | ||||
|   | ||||
| @@ -3,19 +3,17 @@ import style from './Preview.module.css'; | ||||
| import {Spinner} from 'react-bootstrap'; | ||||
| import {Link} from 'react-router-dom'; | ||||
| import GlobalInfos from '../../utils/GlobalInfos'; | ||||
| import {APINode, callAPIPlain} from '../../utils/Api'; | ||||
| import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; | ||||
| import {faPhotoVideo} from '@fortawesome/free-solid-svg-icons'; | ||||
|  | ||||
| interface PreviewProps { | ||||
|     name: string; | ||||
|     picLoader: (callback: (pic: string) => void) => void; | ||||
|     linkPath?: string; | ||||
|     onClick?: () => void; | ||||
|     aspectRatio?: number; | ||||
|     movieId: number; | ||||
| } | ||||
|  | ||||
| interface PreviewState { | ||||
|     picLoaded: boolean | null; | ||||
|     previewpicture: string | null; | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -23,99 +21,55 @@ interface PreviewState { | ||||
|  * floating side by side | ||||
|  */ | ||||
| class Preview extends React.Component<PreviewProps, PreviewState> { | ||||
|     // store the picture to display | ||||
|     pic?: string; | ||||
|  | ||||
|     static readonly DefMinWidth = 266; | ||||
|     static readonly DefMaxWidth = 410; | ||||
|     static readonly DefMinHeight = 150; | ||||
|     static readonly DefMaxHeight = 400; | ||||
|  | ||||
|     constructor(props: PreviewProps) { | ||||
|         super(props); | ||||
|  | ||||
|         this.state = { | ||||
|             picLoaded: null | ||||
|             previewpicture: null | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     componentDidMount(): void { | ||||
|         this.props.picLoader((result) => { | ||||
|             this.pic = result; | ||||
|         callAPIPlain(APINode.Video, {action: 'readThumbnail', movieid: this.props.movieId}, (result) => { | ||||
|             this.setState({ | ||||
|                 picLoaded: result !== '' | ||||
|                 previewpicture: result | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     render(): JSX.Element { | ||||
|         if (this.props.linkPath != null) { | ||||
|             return <Link to={this.props.linkPath}>{this.content()}</Link>; | ||||
|         } else { | ||||
|             return this.content(); | ||||
|         } | ||||
|         const themeStyle = GlobalInfos.getThemeStyle(); | ||||
|         return ( | ||||
|             <Link to={'/player/' + this.props.movieId}> | ||||
|                 <div className={style.videopreview + ' ' + themeStyle.secbackground + ' ' + themeStyle.preview}> | ||||
|                     <div className={style.previewpic}>{this.renderPic()}</div> | ||||
|                     <div className={style.previewText + ' ' + themeStyle.lighttextcolor}>{this.props.name}</div> | ||||
|                 </div> | ||||
|             </Link> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     content(): JSX.Element { | ||||
|         const themeStyle = GlobalInfos.getThemeStyle(); | ||||
|         const ratio = this.props.aspectRatio; | ||||
|         let dimstyle = null; | ||||
|  | ||||
|         // check if aspect ratio is passed | ||||
|         if (ratio != null) { | ||||
|             // if ratio is <1 we need to calc height | ||||
|             if (ratio < 1) { | ||||
|                 const height = Preview.DefMaxWidth * ratio; | ||||
|                 dimstyle = {height: height, width: Preview.DefMaxWidth}; | ||||
|             } else { | ||||
|                 const width = Preview.DefMaxHeight * ratio; | ||||
|                 dimstyle = {width: width, height: Preview.DefMaxHeight}; | ||||
|             } | ||||
|     private renderPic(): JSX.Element { | ||||
|         if (this.state.previewpicture === '') { | ||||
|             return ( | ||||
|                 <FontAwesomeIcon | ||||
|                     style={{ | ||||
|                         color: 'white', | ||||
|                         marginTop: '55px' | ||||
|                     }} | ||||
|                     icon={faPhotoVideo} | ||||
|                     size='5x' | ||||
|                 /> | ||||
|             ); | ||||
|         } else if (this.state.previewpicture === null) { | ||||
|             return ( | ||||
|                 <span className={style.loadAnimation}> | ||||
|                     <Spinner animation='border' /> | ||||
|                 </span> | ||||
|             ); | ||||
|         } else { | ||||
|             return <img className={style.previewimage} src={this.state.previewpicture} alt='Pic loading.' />; | ||||
|         } | ||||
|  | ||||
|         return ( | ||||
|             <div | ||||
|                 className={style.videopreview + ' ' + themeStyle.secbackground + ' ' + themeStyle.preview} | ||||
|                 onClick={this.props.onClick}> | ||||
|                 <div | ||||
|                     style={{maxWidth: dimstyle !== null ? dimstyle.width : Preview.DefMaxWidth}} | ||||
|                     className={style.previewtitle + ' ' + themeStyle.lighttextcolor}> | ||||
|                     {this.props.name} | ||||
|                 </div> | ||||
|                 <div style={dimstyle !== null ? dimstyle : undefined} className={style.previewpic}> | ||||
|                     {this.state.picLoaded === false ? ( | ||||
|                         <FontAwesomeIcon | ||||
|                             style={{ | ||||
|                                 color: 'white', | ||||
|                                 marginTop: '55px' | ||||
|                             }} | ||||
|                             icon={faPhotoVideo} | ||||
|                             size='5x' | ||||
|                         /> | ||||
|                     ) : this.state.picLoaded === null ? ( | ||||
|                         <span className={style.loadAnimation}> | ||||
|                             <Spinner animation='border' /> | ||||
|                         </span> | ||||
|                     ) : ( | ||||
|                         <img | ||||
|                             style={ | ||||
|                                 dimstyle !== null | ||||
|                                     ? dimstyle | ||||
|                                     : { | ||||
|                                           minWidth: Preview.DefMinWidth, | ||||
|                                           maxWidth: Preview.DefMaxWidth, | ||||
|                                           minHeight: Preview.DefMinHeight, | ||||
|                                           maxHeight: Preview.DefMaxHeight | ||||
|                                       } | ||||
|                             } | ||||
|                             src={this.pic} | ||||
|                             alt='Pic loading.' | ||||
|                         /> | ||||
|                     )} | ||||
|                 </div> | ||||
|                 <div className={style.previewbottom} /> | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -5,29 +5,37 @@ import Preview, {TagPreview} from './Preview'; | ||||
|  | ||||
| describe('<Preview/>', function () { | ||||
|     it('renders without crashing ', function () { | ||||
|         const wrapper = shallow(<Preview movieId={1} name='test' picLoader={callback => callback('')}/>); | ||||
|         const wrapper = shallow(<Preview movieId={1}/>); | ||||
|         wrapper.unmount(); | ||||
|     }); | ||||
|  | ||||
|     it('picture rendered correctly', () => { | ||||
|         const func = jest.fn(); | ||||
|         const wrapper = shallow(<Preview movieId={1} name='test' picLoader={callback => { | ||||
|             func(); | ||||
|             callback('42'); | ||||
|         }}/>); | ||||
|     it('picture rendered correctly', done => { | ||||
|         const mockSuccessResponse = 'testsrc'; | ||||
|         const mockJsonPromise = Promise.resolve(mockSuccessResponse); | ||||
|         const mockFetchPromise = Promise.resolve({ | ||||
|             text: () => mockJsonPromise | ||||
|         }); | ||||
|         global.fetch = jest.fn().mockImplementation(() => mockFetchPromise); | ||||
|  | ||||
|         // expect picloader tobe called once | ||||
|         expect(func).toHaveBeenCalledTimes(1) | ||||
|         const wrapper = shallow(<Preview name='test' movieId={1}/>); | ||||
|  | ||||
|         // now called 1 times | ||||
|         expect(global.fetch).toHaveBeenCalledTimes(1); | ||||
|  | ||||
|         process.nextTick(() => { | ||||
|             // received picture should be rendered into wrapper | ||||
|             expect(wrapper.find('.previewimage').props().src).not.toBeNull(); | ||||
|             // check if preview title renders correctly | ||||
|             expect(wrapper.find('.previewtitle').text()).toBe('test'); | ||||
|  | ||||
|             global.fetch.mockClear(); | ||||
|             done(); | ||||
|         }); | ||||
|  | ||||
|         // received picture should be rendered into wrapper | ||||
|         expect(wrapper.find('img').props().src).toBe('42'); | ||||
|         // check if preview title renders correctly | ||||
|         expect(wrapper.find('.previewtitle').text()).toBe('test'); | ||||
|     }); | ||||
|  | ||||
|     it('spinner loads correctly', function () { | ||||
|         // if callback is never called --> infinite spinner | ||||
|         const wrapper = shallow(<Preview movieId={1} name='test' picLoader={callback => {}}/>); | ||||
|         const wrapper = shallow(<Preview movieId={1}/>); | ||||
|  | ||||
|         // expect load animation to be visible | ||||
|         expect(wrapper.find('.loadAnimation')).toHaveLength(1); | ||||
|   | ||||
| @@ -17,7 +17,7 @@ class Tag extends React.Component<props> { | ||||
|         if (this.props.onclick) { | ||||
|             return this.renderButton(); | ||||
|         } else { | ||||
|             return <Link to={'/media/categories/' + this.props.tagInfo.TagId}>{this.renderButton()}</Link>; | ||||
|             return <Link to={'/categories/' + this.props.tagInfo.TagId}>{this.renderButton()}</Link>; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| .maincontent { | ||||
|     float: left; | ||||
|     width: 70%; | ||||
|     flex: 1; | ||||
| } | ||||
| } | ||||
| @@ -7,4 +7,24 @@ describe('<VideoContainer/>', function () { | ||||
|         const wrapper = shallow(<VideoContainer data={[]}/>); | ||||
|         wrapper.unmount(); | ||||
|     }); | ||||
|  | ||||
|     it('inserts tiles correctly if enough available', () => { | ||||
|         const wrapper = shallow(<VideoContainer data={[ | ||||
|             {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {} | ||||
|         ]}/>); | ||||
|         expect(wrapper.find('Preview')).toHaveLength(16); | ||||
|     }); | ||||
|  | ||||
|     it('inserts tiles correctly if not enough available', () => { | ||||
|         const wrapper = shallow(<VideoContainer data={[ | ||||
|             {}, {}, {}, {} | ||||
|         ]}/>); | ||||
|         expect(wrapper.find('Preview')).toHaveLength(4); | ||||
|     }); | ||||
|  | ||||
|     it('no items available', () => { | ||||
|         const wrapper = shallow(<VideoContainer data={[]}/>); | ||||
|         expect(wrapper.find('Preview')).toHaveLength(0); | ||||
|         expect(wrapper.find('.maincontent').text()).toBe('no items to show!'); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -1,39 +1,89 @@ | ||||
| import React from 'react'; | ||||
| import Preview from '../Preview/Preview'; | ||||
| import style from './VideoContainer.module.css'; | ||||
| import {VideoTypes} from '../../types/ApiTypes'; | ||||
| import DynamicContentContainer from '../DynamicContentContainer/DynamicContentContainer'; | ||||
| import {APINode, callAPIPlain} from '../../utils/Api'; | ||||
|  | ||||
| interface Props { | ||||
|     data: VideoTypes.VideoUnloadedType[]; | ||||
|     children?: JSX.Element; | ||||
| } | ||||
|  | ||||
| const VideoContainer = (props: Props): JSX.Element => { | ||||
|     return ( | ||||
|         <DynamicContentContainer | ||||
|             renderElement={(el): JSX.Element => ( | ||||
|                 <Preview | ||||
|                     key={el.MovieId} | ||||
|                     aspectRatio={el.Ratio > 0 ? el.Ratio : undefined} | ||||
|                     picLoader={(callback: (pic: string) => void): void => { | ||||
|                         callAPIPlain( | ||||
|                             APINode.Video, | ||||
|                             { | ||||
|                                 action: 'readThumbnail', | ||||
|                                 Movieid: el.MovieId | ||||
|                             }, | ||||
|                             (result) => callback(result) | ||||
|                         ); | ||||
|                     }} | ||||
|                     name={el.MovieName} | ||||
|                     linkPath={'/media/player/' + el.MovieId} | ||||
|                 /> | ||||
|             )} | ||||
|             data={props.data}> | ||||
|             {props.children} | ||||
|         </DynamicContentContainer> | ||||
|     ); | ||||
| }; | ||||
| interface state { | ||||
|     loadeditems: VideoTypes.VideoUnloadedType[]; | ||||
|     selectionnr: number; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * A videocontainer storing lots of Preview elements | ||||
|  * includes scroll handling and loading of preview infos | ||||
|  */ | ||||
| class VideoContainer extends React.Component<Props, state> { | ||||
|     // stores current index of loaded elements | ||||
|     loadindex: number = 0; | ||||
|  | ||||
|     constructor(props: Props) { | ||||
|         super(props); | ||||
|  | ||||
|         this.state = { | ||||
|             loadeditems: [], | ||||
|             selectionnr: 0 | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     componentDidMount(): void { | ||||
|         document.addEventListener('scroll', this.trackScrolling); | ||||
|  | ||||
|         this.loadPreviewBlock(16); | ||||
|     } | ||||
|  | ||||
|     render(): JSX.Element { | ||||
|         return ( | ||||
|             <div className={style.maincontent}> | ||||
|                 {this.state.loadeditems.map((elem) => ( | ||||
|                     <Preview key={elem.MovieId} name={elem.MovieName} movieId={elem.MovieId} /> | ||||
|                 ))} | ||||
|                 {/*todo css for no items to show*/} | ||||
|                 {this.state.loadeditems.length === 0 ? 'no items to show!' : null} | ||||
|                 {this.props.children} | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     componentWillUnmount(): void { | ||||
|         this.setState({}); | ||||
|         // unbind scroll listener when unmounting component | ||||
|         document.removeEventListener('scroll', this.trackScrolling); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * load previews to the container | ||||
|      * @param nr number of previews to load | ||||
|      */ | ||||
|     loadPreviewBlock(nr: number): void { | ||||
|         console.log('loadpreviewblock called ...'); | ||||
|         let ret = []; | ||||
|         for (let i = 0; i < nr; i++) { | ||||
|             // only add if not end | ||||
|             if (this.props.data.length > this.loadindex + i) { | ||||
|                 ret.push(this.props.data[this.loadindex + i]); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         this.setState({ | ||||
|             loadeditems: [...this.state.loadeditems, ...ret] | ||||
|         }); | ||||
|  | ||||
|         this.loadindex += nr; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * scroll event handler -> load new previews if on bottom | ||||
|      */ | ||||
|     trackScrolling = (): void => { | ||||
|         // comparison if current scroll position is on bottom --> 200 is bottom offset to trigger load | ||||
|         if (window.innerHeight + document.documentElement.scrollTop + 200 >= document.documentElement.offsetHeight) { | ||||
|             this.loadPreviewBlock(8); | ||||
|         } | ||||
|     }; | ||||
| } | ||||
|  | ||||
| export default VideoContainer; | ||||
|   | ||||
| @@ -1,19 +1,13 @@ | ||||
| import React from 'react'; | ||||
| import ReactDOM from 'react-dom'; | ||||
| import App from './App'; | ||||
| import {BrowserRouter} from 'react-router-dom'; | ||||
| import {FeatureContextProvider} from './utils/context/FeatureContext'; | ||||
|  | ||||
| // don't allow console logs within production env | ||||
| global.console.log = process.env.NODE_ENV !== 'development' ? (_: string | number | boolean): void => {} : global.console.log; | ||||
|  | ||||
| ReactDOM.render( | ||||
|     <React.StrictMode> | ||||
|         <BrowserRouter> | ||||
|             <FeatureContextProvider> | ||||
|                 <App /> | ||||
|             </FeatureContextProvider> | ||||
|         </BrowserRouter> | ||||
|         <App /> | ||||
|     </React.StrictMode>, | ||||
|     document.getElementById('root') | ||||
| ); | ||||
|   | ||||
| @@ -8,6 +8,20 @@ describe('<ActorOverviewPage/>', function () { | ||||
|         wrapper.unmount(); | ||||
|     }); | ||||
|  | ||||
|     it('test inerstion of actor tiles', function () { | ||||
|         const wrapper = shallow(<ActorOverviewPage/>); | ||||
|  | ||||
|         wrapper.setState({ | ||||
|             actors: [{ | ||||
|                 thumbnail: '', | ||||
|                 name: 'testname', | ||||
|                 actor_id: 42 | ||||
|             }] | ||||
|         }); | ||||
|  | ||||
|         expect(wrapper.find('ActorTile')).toHaveLength(1); | ||||
|     }); | ||||
|  | ||||
|     it('test newtagpopup visibility', function () { | ||||
|         const wrapper = shallow(<ActorOverviewPage/>); | ||||
|  | ||||
|   | ||||
| @@ -4,10 +4,9 @@ import {ActorType} from '../../types/VideoTypes'; | ||||
| import ActorTile from '../../elements/ActorTile/ActorTile'; | ||||
| import PageTitle from '../../elements/PageTitle/PageTitle'; | ||||
| import SideBar from '../../elements/SideBar/SideBar'; | ||||
| // import style from './ActorOverviewPage.module.css'; | ||||
| import style from './ActorOverviewPage.module.css'; | ||||
| import {Button} from '../../elements/GPElements/Button'; | ||||
| import NewActorPopup from '../../elements/Popups/NewActorPopup/NewActorPopup'; | ||||
| import DynamicContentContainer from '../../elements/DynamicContentContainer/DynamicContentContainer'; | ||||
|  | ||||
| interface Props {} | ||||
|  | ||||
| @@ -37,12 +36,11 @@ class ActorOverviewPage extends React.Component<Props, state> { | ||||
|                 <SideBar> | ||||
|                     <Button title='Add Actor' onClick={(): void => this.setState({NActorPopupVisible: true})} /> | ||||
|                 </SideBar> | ||||
|                 <DynamicContentContainer | ||||
|                     renderElement={(el): JSX.Element => <ActorTile key={el.ActorId} actor={el} />} | ||||
|                     data={this.state.actors} | ||||
|                     initialLoadNr={36} | ||||
|                 /> | ||||
|  | ||||
|                 <div className={style.container}> | ||||
|                     {this.state.actors.map((el) => ( | ||||
|                         <ActorTile key={el.ActorId} actor={el} /> | ||||
|                     ))} | ||||
|                 </div> | ||||
|                 {this.state.NActorPopupVisible ? ( | ||||
|                     <NewActorPopup | ||||
|                         onHide={(): void => { | ||||
|   | ||||
| @@ -37,7 +37,7 @@ export class ActorPage extends React.Component<Props, state> { | ||||
|             <> | ||||
|                 <PageTitle title={this.state.actor.Name} subtitle={this.state.data ? this.state.data.length + ' videos' : null}> | ||||
|                     <span className={style.overviewbutton}> | ||||
|                         <Link to='/media/actors'> | ||||
|                         <Link to='/actors'> | ||||
|                             <Button onClick={(): void => {}} title='Go to Actor overview' /> | ||||
|                         </Link> | ||||
|                     </span> | ||||
| @@ -48,7 +48,7 @@ export class ActorPage extends React.Component<Props, state> { | ||||
|                     </div> | ||||
|                     <SideBarTitle>Attention: This is an early preview!</SideBarTitle> | ||||
|                 </SideBar> | ||||
|                 <VideoContainer data={this.state.data} /> | ||||
|                 {this.state.data.length !== 0 ? <VideoContainer data={this.state.data} /> : <div>No Data found!</div>} | ||||
|             </> | ||||
|         ); | ||||
|     } | ||||
| @@ -72,10 +72,6 @@ export class ActorPage extends React.Component<Props, state> { | ||||
|                     data: result.Videos ? result.Videos : [], | ||||
|                     actor: result.Info | ||||
|                 }); | ||||
|             }, | ||||
|             (_) => { | ||||
|                 // if there is an load error redirect to home page | ||||
|                 this.props.history.push('/'); | ||||
|             } | ||||
|         ); | ||||
|     } | ||||
|   | ||||
| @@ -11,8 +11,10 @@ describe('<AuthenticationPage/>', function () { | ||||
|  | ||||
|     it('test button click', function () { | ||||
|         const func = jest.fn(); | ||||
|         const wrapper = shallow(<AuthenticationPage />); | ||||
|         wrapper.instance().authenticate = func; | ||||
|         const wrapper = shallow(<AuthenticationPage onSuccessLogin={func}/>); | ||||
|         wrapper.instance().authenticate = jest.fn(() => { | ||||
|             wrapper.instance().props.onSuccessLogin() | ||||
|         }); | ||||
|         wrapper.setState({pwdText: 'testpwd'}); | ||||
|  | ||||
|         wrapper.find('Button').simulate('click'); | ||||
| @@ -20,16 +22,35 @@ describe('<AuthenticationPage/>', function () { | ||||
|         expect(func).toHaveBeenCalledTimes(1); | ||||
|     }); | ||||
|  | ||||
|     it('test keyenter', function () { | ||||
|     it('test fail authenticate', function () { | ||||
|         const events = mockKeyPress(); | ||||
|  | ||||
|         const helpers = require('../../utils/Api'); | ||||
|         helpers.refreshAPIToken = jest.fn().mockImplementation((callback, force, pwd) => { | ||||
|             callback('there was an error') | ||||
|         }); | ||||
|  | ||||
|         const wrapper = shallow(<AuthenticationPage/>); | ||||
|  | ||||
|         const func = jest.fn(); | ||||
|         wrapper.instance().authenticate = func; | ||||
|         events.keyup({key: 'Enter'}); | ||||
|  | ||||
|         expect(wrapper.state().wrongPWDInfo).toBe(true); | ||||
|     }); | ||||
|  | ||||
|     it('test success authenticate', function () { | ||||
|         const events = mockKeyPress(); | ||||
|         const func = jest.fn() | ||||
|  | ||||
|         const helpers = require('../../utils/Api'); | ||||
|         helpers.refreshAPIToken = jest.fn().mockImplementation((callback, force, pwd) => { | ||||
|             callback('') | ||||
|         }); | ||||
|  | ||||
|         const wrapper = shallow(<AuthenticationPage onSuccessLogin={func}/>); | ||||
|  | ||||
|         events.keyup({key: 'Enter'}); | ||||
|  | ||||
|         expect(wrapper.state().wrongPWDInfo).toBe(false); | ||||
|         expect(func).toHaveBeenCalledTimes(1); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -2,18 +2,18 @@ import React from 'react'; | ||||
| import {Button} from '../../elements/GPElements/Button'; | ||||
| import style from './AuthenticationPage.module.css'; | ||||
| import {addKeyHandler, removeKeyHandler} from '../../utils/ShortkeyHandler'; | ||||
| import {refreshAPIToken} from '../../utils/Api'; | ||||
| import {faTimes} from '@fortawesome/free-solid-svg-icons'; | ||||
| import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; | ||||
| import {LoginContext, LoginState} from '../../utils/context/LoginContext'; | ||||
| import {APINode, callApiUnsafe} from '../../utils/Api'; | ||||
| import {cookie, Token} from '../../utils/context/Cookie'; | ||||
|  | ||||
| interface state { | ||||
|     pwdText: string; | ||||
|     wrongPWDInfo: boolean; | ||||
| } | ||||
|  | ||||
| interface Props {} | ||||
| interface Props { | ||||
|     onSuccessLogin: () => void; | ||||
| } | ||||
|  | ||||
| class AuthenticationPage extends React.Component<Props, state> { | ||||
|     constructor(props: Props) { | ||||
| @@ -36,8 +36,6 @@ class AuthenticationPage extends React.Component<Props, state> { | ||||
|         removeKeyHandler(this.keypress); | ||||
|     } | ||||
|  | ||||
|     static contextType = LoginContext; | ||||
|  | ||||
|     render(): JSX.Element { | ||||
|         return ( | ||||
|             <> | ||||
| @@ -78,18 +76,21 @@ class AuthenticationPage extends React.Component<Props, state> { | ||||
|      * request a new token and check if pwd was valid | ||||
|      */ | ||||
|     authenticate(): void { | ||||
|         callApiUnsafe( | ||||
|             APINode.Login, | ||||
|             {action: 'login', Password: this.state.pwdText}, | ||||
|             (r: Token) => { | ||||
|                 cookie.Store(r); | ||||
|         refreshAPIToken( | ||||
|             (error) => { | ||||
|                 if (error !== '') { | ||||
|                     this.setState({wrongPWDInfo: true}); | ||||
|  | ||||
|                 this.context.setLoginState(LoginState.LoggedIn); | ||||
|                     // set timeout to make the info auto-disappearing | ||||
|                     setTimeout(() => { | ||||
|                         this.setState({wrongPWDInfo: false}); | ||||
|                     }, 2000); | ||||
|                 } else { | ||||
|                     this.props.onSuccessLogin(); | ||||
|                 } | ||||
|             }, | ||||
|             () => { | ||||
|                 this.setState({wrongPWDInfo: true}); | ||||
|                 setTimeout(() => this.setState({wrongPWDInfo: false}), 2000); | ||||
|             } | ||||
|             true, | ||||
|             this.state.pwdText | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|   | ||||
							
								
								
									
										10
									
								
								src/pages/CategoryPage/CategoryPage.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/pages/CategoryPage/CategoryPage.test.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import {shallow} from 'enzyme'; | ||||
| import React from 'react'; | ||||
| import CategoryPage from './CategoryPage'; | ||||
|  | ||||
| describe('<CategoryPage/>', function () { | ||||
|     it('renders without crashing ', function () { | ||||
|         const wrapper = shallow(<CategoryPage/>); | ||||
|         wrapper.unmount(); | ||||
|     }); | ||||
| }); | ||||
| @@ -1,5 +1,5 @@ | ||||
| import React from 'react'; | ||||
| import {Route, Switch, useRouteMatch} from 'react-router-dom'; | ||||
| import {Route, Switch} from 'react-router-dom'; | ||||
| import {CategoryViewWR} from './CategoryView'; | ||||
| import TagView from './TagView'; | ||||
|  | ||||
| @@ -7,19 +7,19 @@ import TagView from './TagView'; | ||||
|  * Component for Category Page | ||||
|  * Contains a Tag Overview and loads specific Tag videos in VideoContainer | ||||
|  */ | ||||
| const CategoryPage = (): JSX.Element => { | ||||
|     const match = useRouteMatch(); | ||||
|  | ||||
|     return ( | ||||
|         <Switch> | ||||
|             <Route exact path={`${match.url}/:id`}> | ||||
|                 <CategoryViewWR /> | ||||
|             </Route> | ||||
|             <Route exact path={`${match.url}/`}> | ||||
|                 <TagView /> | ||||
|             </Route> | ||||
|         </Switch> | ||||
|     ); | ||||
| }; | ||||
| class CategoryPage extends React.Component { | ||||
|     render(): JSX.Element { | ||||
|         return ( | ||||
|             <Switch> | ||||
|                 <Route path='/categories/:id'> | ||||
|                     <CategoryViewWR /> | ||||
|                 </Route> | ||||
|                 <Route path='/categories'> | ||||
|                     <TagView /> | ||||
|                 </Route> | ||||
|             </Switch> | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default CategoryPage; | ||||
|   | ||||
| @@ -4,9 +4,7 @@ import {CategoryView} from './CategoryView'; | ||||
|  | ||||
| describe('<CategoryView/>', function () { | ||||
|     function instance() { | ||||
|         const inst = shallow(<CategoryView match={{params: {id: 10}}} history={{push: jest.fn()}}/>); | ||||
|         inst.setState({loaded: true}); | ||||
|         return inst; | ||||
|         return shallow(<CategoryView match={{params: {id: 10}}} history={{push: jest.fn()}}/>); | ||||
|     } | ||||
|  | ||||
|     it('renders without crashing ', function () { | ||||
|   | ||||
| @@ -10,14 +10,12 @@ import Tag from '../../elements/Tag/Tag'; | ||||
| import {DefaultTags, GeneralSuccess} from '../../types/GeneralTypes'; | ||||
| import {Button} from '../../elements/GPElements/Button'; | ||||
| import SubmitPopup from '../../elements/Popups/SubmitPopup/SubmitPopup'; | ||||
| import {Spinner} from 'react-bootstrap'; | ||||
|  | ||||
| interface CategoryViewProps extends RouteComponentProps<{id: string}> {} | ||||
|  | ||||
| interface CategoryViewState { | ||||
|     loaded: boolean; | ||||
|     submitForceDelete: boolean; | ||||
|     TagName: string; | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -31,8 +29,7 @@ export class CategoryView extends React.Component<CategoryViewProps, CategoryVie | ||||
|  | ||||
|         this.state = { | ||||
|             loaded: false, | ||||
|             submitForceDelete: false, | ||||
|             TagName: '' | ||||
|             submitForceDelete: false | ||||
|         }; | ||||
|     } | ||||
|  | ||||
| @@ -53,13 +50,9 @@ export class CategoryView extends React.Component<CategoryViewProps, CategoryVie | ||||
|     } | ||||
|  | ||||
|     render(): JSX.Element { | ||||
|         if (!this.state.loaded) { | ||||
|             return <Spinner animation='border' />; | ||||
|         } | ||||
|  | ||||
|         return ( | ||||
|             <> | ||||
|                 <PageTitle title={this.state.TagName} subtitle={this.videodata.length + ' Videos'} /> | ||||
|                 <PageTitle title='Categories' subtitle={this.videodata.length + ' Videos'} /> | ||||
|  | ||||
|                 <SideBar> | ||||
|                     <SideBarTitle>Default Tags:</SideBarTitle> | ||||
| @@ -112,19 +105,10 @@ export class CategoryView extends React.Component<CategoryViewProps, CategoryVie | ||||
|      * @param id tagid | ||||
|      */ | ||||
|     private fetchVideoData(id: number): void { | ||||
|         callAPI( | ||||
|             APINode.Video, | ||||
|             {action: 'getMovies', Tag: id}, | ||||
|             (result: {Videos: VideoTypes.VideoUnloadedType[]; TagName: string}) => { | ||||
|                 this.videodata = result.Videos; | ||||
|                 this.setState({loaded: true, TagName: result.TagName}); | ||||
|             }, | ||||
|             (e) => { | ||||
|                 console.log(e); | ||||
|                 // if there is an load error redirect to home page | ||||
|                 // this.props.history.push('/'); | ||||
|             } | ||||
|         ); | ||||
|         callAPI<VideoTypes.VideoUnloadedType[]>(APINode.Video, {action: 'getMovies', tag: id}, (result) => { | ||||
|             this.videodata = result; | ||||
|             this.setState({loaded: true}); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -8,6 +8,13 @@ describe('<TagView/>', function () { | ||||
|         wrapper.unmount(); | ||||
|     }); | ||||
|  | ||||
|     it('test Tag insertion', function () { | ||||
|         const wrapper = shallow(<TagView/>); | ||||
|         wrapper.setState({loadedtags: [{tag_name: 'test', tag_id: 42}]}); | ||||
|  | ||||
|         expect(wrapper.find('TagPreview')).toHaveLength(1); | ||||
|     }); | ||||
|  | ||||
|     it('test new tag popup', function () { | ||||
|         const wrapper = shallow(<TagView/>); | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import {TagType} from '../../types/VideoTypes'; | ||||
| import React from 'react'; | ||||
| import videocontainerstyle from '../../elements/VideoContainer/VideoContainer.module.css'; | ||||
| import {Link} from 'react-router-dom'; | ||||
| import {TagPreview} from '../../elements/Preview/Preview'; | ||||
| import {APINode, callAPI} from '../../utils/Api'; | ||||
| @@ -8,7 +9,6 @@ import SideBar, {SideBarTitle} from '../../elements/SideBar/SideBar'; | ||||
| import Tag from '../../elements/Tag/Tag'; | ||||
| import {DefaultTags} from '../../types/GeneralTypes'; | ||||
| import NewTagPopup from '../../elements/Popups/NewTagPopup/NewTagPopup'; | ||||
| import DynamicContentContainer from '../../elements/DynamicContentContainer/DynamicContentContainer'; | ||||
|  | ||||
| interface TagViewState { | ||||
|     loadedtags: TagType[]; | ||||
| @@ -53,15 +53,15 @@ class TagView extends React.Component<Props, TagViewState> { | ||||
|                         Add a new Tag! | ||||
|                     </button> | ||||
|                 </SideBar> | ||||
|                 <DynamicContentContainer | ||||
|                     data={this.state.loadedtags} | ||||
|                     renderElement={(m): JSX.Element => ( | ||||
|                         <Link to={'/media/categories/' + m.TagId} key={m.TagId}> | ||||
|                             <TagPreview name={m.TagName} /> | ||||
|                         </Link> | ||||
|                     )} | ||||
|                     initialLoadNr={20} | ||||
|                 /> | ||||
|                 <div className={videocontainerstyle.maincontent}> | ||||
|                     {this.state.loadedtags | ||||
|                         ? this.state.loadedtags.map((m) => ( | ||||
|                               <Link to={'/categories/' + m.TagId} key={m.TagId}> | ||||
|                                   <TagPreview name={m.TagName} /> | ||||
|                               </Link> | ||||
|                           )) | ||||
|                         : 'loading'} | ||||
|                 </div> | ||||
|                 {this.handlePopups()} | ||||
|             </> | ||||
|         ); | ||||
|   | ||||
| @@ -7,61 +7,3 @@ | ||||
|     float: right; | ||||
|     margin-top: 25px; | ||||
| } | ||||
|  | ||||
| .sortbyLabel { | ||||
|     color: grey; | ||||
|     margin-right: 5px; | ||||
|     margin-left: 25px; | ||||
| } | ||||
|  | ||||
| /* Style The Dropdown Button */ | ||||
| .dropbtn { | ||||
|     display: flex; | ||||
|     justify-content: center; | ||||
|     align-items: center; | ||||
|     flex-direction: row; | ||||
|     height: 100%; | ||||
|     cursor: pointer; | ||||
|     color: white; | ||||
|     text-align: center; | ||||
| } | ||||
|  | ||||
| /* The container <div> - needed to position the dropdown content */ | ||||
| .dropdown { | ||||
|     position: relative; | ||||
|     display: inline-block; | ||||
| } | ||||
|  | ||||
| /* Dropdown Content (Hidden by Default) */ | ||||
| .dropdownContent { | ||||
|     display: none; | ||||
|     position: absolute; | ||||
|     background-color: #f9f9f9; | ||||
|     min-width: 160px; | ||||
|     box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); | ||||
|     z-index: 1; | ||||
| } | ||||
|  | ||||
| /* Links inside the dropdown */ | ||||
| .dropdownContent span { | ||||
|     color: black; | ||||
|     padding: 12px 16px; | ||||
|     text-decoration: none; | ||||
|     display: block; | ||||
|     cursor: pointer; | ||||
| } | ||||
|  | ||||
| /* Change color of dropdown links on hover */ | ||||
| .dropdownContent span:hover { | ||||
|     background-color: #f1f1f1; | ||||
| } | ||||
|  | ||||
| /* Show the dropdown menu on hover */ | ||||
| .dropdown:hover .dropdownContent { | ||||
|     display: block; | ||||
| } | ||||
|  | ||||
| /* Change the background color of the dropdown button when the dropdown content is shown */ | ||||
| .dropdown:hover .dropbtn { | ||||
|     background-color: #3574fe; | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,8 @@ | ||||
| import {shallow} from 'enzyme'; | ||||
| import React from 'react'; | ||||
| import {HomePage, SortBy} from './HomePage'; | ||||
| import {HomePage} from './HomePage'; | ||||
| import VideoContainer from '../../elements/VideoContainer/VideoContainer'; | ||||
| import {SearchHandling} from './SearchHandling'; | ||||
| import exp from "constants"; | ||||
| import {DefaultTags} from "../../types/GeneralTypes"; | ||||
|  | ||||
| describe('<HomePage/>', function () { | ||||
|     it('renders without crashing ', function () { | ||||
| @@ -12,6 +10,21 @@ describe('<HomePage/>', function () { | ||||
|         wrapper.unmount(); | ||||
|     }); | ||||
|  | ||||
|     it('test data insertion', function () { | ||||
|         const wrapper = shallow(<HomePage/>); | ||||
|  | ||||
|         expect(wrapper.find('VideoContainer')).toHaveLength(0); | ||||
|  | ||||
|         wrapper.setState({ | ||||
|             data: [ | ||||
|                 {}, {} | ||||
|             ] | ||||
|         }); | ||||
|  | ||||
|         // there shoud be loaded the Videocontainer element into dom after fetching videos correctly | ||||
|         expect(wrapper.find('VideoContainer')).toHaveLength(1); | ||||
|     }); | ||||
|  | ||||
|     it('test title and nr insertions', function () { | ||||
|         const wrapper = shallow(<HomePage/>); | ||||
|  | ||||
| @@ -25,6 +38,23 @@ describe('<HomePage/>', function () { | ||||
|         expect(wrapper.find('PageTitle').props().subtitle).toBe('testsubtitle - 42'); | ||||
|     }); | ||||
|  | ||||
|     it('test search field', done => { | ||||
|         global.fetch = global.prepareFetchApi([{}, {}]); | ||||
|  | ||||
|         const wrapper = shallow(<HomePage/>); | ||||
|  | ||||
|         wrapper.find('[data-testid="searchtextfield"]').simulate('change', {target: {value: 'testvalue'}}); | ||||
|         wrapper.find('[data-testid="searchbtnsubmit"]').simulate('click'); | ||||
|  | ||||
|         process.nextTick(() => { | ||||
|             // state to be set correctly with response | ||||
|             expect(wrapper.state().selectionnr).toBe(2); | ||||
|  | ||||
|             global.fetch.mockClear(); | ||||
|             done(); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     it('test form submit', () => { | ||||
|         const func = jest.fn(); | ||||
|         const wrapper = shallow(<HomePage/>); | ||||
| @@ -57,7 +87,7 @@ describe('<HomePage/>', function () { | ||||
|     }); | ||||
|  | ||||
|     it('test tag click', done => { | ||||
|         global.fetch = prepareFetchApi({Videos: ['test1', 'test2'], TagName: 'all'}); | ||||
|         global.fetch = prepareFetchApi(['test1', 'test2']); | ||||
|  | ||||
|         const wrapper = shallow(<HomePage/>); | ||||
|  | ||||
| @@ -85,20 +115,6 @@ describe('<HomePage/>', function () { | ||||
|  | ||||
|         testBtn(tags.first()); | ||||
|     }); | ||||
|  | ||||
|     it('test sortby type change', function () { | ||||
|         const wrapper = shallow(<HomePage/>); | ||||
|  | ||||
|         // expect those default values | ||||
|         expect(wrapper.state().sortby).toBe('Date Added'); | ||||
|         expect(wrapper.instance().sortState).toBe(SortBy.date); | ||||
|         expect(wrapper.instance().tagState).toBe(DefaultTags.all); | ||||
|  | ||||
|         wrapper.instance().onDropDownItemClick(SortBy.name, 'namesort'); | ||||
|  | ||||
|         expect(wrapper.state().sortby).toBe('namesort'); | ||||
|         expect(wrapper.instance().sortState).toBe(SortBy.name); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| describe('<SearchHandling/>', () => { | ||||
|   | ||||
| @@ -11,16 +11,6 @@ import {RouteComponentProps} from 'react-router'; | ||||
| import SearchHandling from './SearchHandling'; | ||||
| import {VideoTypes} from '../../types/ApiTypes'; | ||||
| import {DefaultTags} from '../../types/GeneralTypes'; | ||||
| import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; | ||||
| import {faSortDown} from '@fortawesome/free-solid-svg-icons'; | ||||
|  | ||||
| export enum SortBy { | ||||
|     date, | ||||
|     likes, | ||||
|     random, | ||||
|     name, | ||||
|     length | ||||
| } | ||||
|  | ||||
| interface Props extends RouteComponentProps {} | ||||
|  | ||||
| @@ -29,7 +19,6 @@ interface state { | ||||
|     subtitle: string; | ||||
|     data: VideoTypes.VideoUnloadedType[]; | ||||
|     selectionnr: number; | ||||
|     sortby: string; | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -53,48 +42,32 @@ export class HomePage extends React.Component<Props, state> { | ||||
|             }, | ||||
|             subtitle: 'All Videos', | ||||
|             data: [], | ||||
|             selectionnr: 0, | ||||
|             sortby: 'Date Added' | ||||
|             selectionnr: 0 | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     sortState = SortBy.random; | ||||
|     tagState = DefaultTags.all; | ||||
|  | ||||
|     componentDidMount(): void { | ||||
|         // initial get of all videos | ||||
|         this.fetchVideoData(); | ||||
|         this.fetchVideoData(DefaultTags.all.TagId); | ||||
|         this.fetchStartData(); | ||||
|     } | ||||
|  | ||||
|     render(): JSX.Element { | ||||
|         return ( | ||||
|             <Switch> | ||||
|                 <Route path='/media/search/:name'> | ||||
|                     <SearchHandling /> | ||||
|                 </Route> | ||||
|                 <Route path='/media/'>{this.homepageContent()}</Route> | ||||
|             </Switch> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * fetch available videos for specified tag | ||||
|      * this function clears all preview elements an reloads gravity with tag | ||||
|      * | ||||
|      * @param tag tag to fetch videos | ||||
|      */ | ||||
|     fetchVideoData(): void { | ||||
|         callAPI( | ||||
|             APINode.Video, | ||||
|             {action: 'getMovies', Tag: this.tagState.TagId, Sort: this.sortState}, | ||||
|             (result: {Videos: VideoTypes.VideoUnloadedType[]; TagName: string}) => { | ||||
|                 this.setState({ | ||||
|                     data: result.Videos, | ||||
|                     selectionnr: result.Videos.length | ||||
|                 }); | ||||
|             } | ||||
|         ); | ||||
|     fetchVideoData(tag: number): void { | ||||
|         callAPI(APINode.Video, {action: 'getMovies', tag: tag}, (result: VideoTypes.VideoUnloadedType[]) => { | ||||
|             this.setState({ | ||||
|                 data: [] | ||||
|             }); | ||||
|             this.setState({ | ||||
|                 data: result, | ||||
|                 selectionnr: result.length | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -106,119 +79,91 @@ export class HomePage extends React.Component<Props, state> { | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * main content of homepage | ||||
|      */ | ||||
|     homepageContent(): JSX.Element { | ||||
|     render(): JSX.Element { | ||||
|         return ( | ||||
|             <> | ||||
|                 <PageTitle title='Home Page' subtitle={this.state.subtitle + ' - ' + this.state.selectionnr}> | ||||
|                     <form | ||||
|                         className={'form-inline ' + style.searchform} | ||||
|                         onSubmit={(e): void => { | ||||
|                             e.preventDefault(); | ||||
|                             this.props.history.push('/media/search/' + this.keyword); | ||||
|                         }}> | ||||
|                         <input | ||||
|                             data-testid='searchtextfield' | ||||
|                             className='form-control mr-sm-2' | ||||
|                             type='text' | ||||
|                             placeholder='Search' | ||||
|                             onChange={(e): void => { | ||||
|                                 this.keyword = e.target.value; | ||||
|                             }} | ||||
|                         /> | ||||
|                         <button data-testid='searchbtnsubmit' className='btn btn-success' type='submit'> | ||||
|                             Search | ||||
|                         </button> | ||||
|                     </form> | ||||
|                 </PageTitle> | ||||
|                 <SideBar> | ||||
|                     <SideBarTitle>Infos:</SideBarTitle> | ||||
|                     <Line /> | ||||
|                     <SideBarItem> | ||||
|                         <b>{this.state.sideinfo.VideoNr}</b> Videos Total! | ||||
|                     </SideBarItem> | ||||
|                     <SideBarItem> | ||||
|                         <b>{this.state.sideinfo.FullHdNr}</b> FULL-HD Videos! | ||||
|                     </SideBarItem> | ||||
|                     <SideBarItem> | ||||
|                         <b>{this.state.sideinfo.HDNr}</b> HD Videos! | ||||
|                     </SideBarItem> | ||||
|                     <SideBarItem> | ||||
|                         <b>{this.state.sideinfo.SDNr}</b> SD Videos! | ||||
|                     </SideBarItem> | ||||
|                     <SideBarItem> | ||||
|                         <b>{this.state.sideinfo.DifferentTags}</b> different Tags! | ||||
|                     </SideBarItem> | ||||
|                     <Line /> | ||||
|                     <SideBarTitle>Default Tags:</SideBarTitle> | ||||
|                     <Tag | ||||
|                         tagInfo={{TagName: 'All', TagId: DefaultTags.all.TagId}} | ||||
|                         onclick={(): void => { | ||||
|                             this.tagState = DefaultTags.all; | ||||
|                             this.fetchVideoData(); | ||||
|                             this.setState({subtitle: 'All Videos'}); | ||||
|                         }} | ||||
|                     /> | ||||
|                     <Tag | ||||
|                         tagInfo={{TagName: 'Full Hd', TagId: DefaultTags.fullhd.TagId}} | ||||
|                         onclick={(): void => { | ||||
|                             this.tagState = DefaultTags.fullhd; | ||||
|                             this.fetchVideoData(); | ||||
|                             this.setState({subtitle: 'Full Hd Videos'}); | ||||
|                         }} | ||||
|                     /> | ||||
|                     <Tag | ||||
|                         tagInfo={{TagName: 'Low Quality', TagId: DefaultTags.lowq.TagId}} | ||||
|                         onclick={(): void => { | ||||
|                             this.tagState = DefaultTags.lowq; | ||||
|                             this.fetchVideoData(); | ||||
|                             this.setState({subtitle: 'Low Quality Videos'}); | ||||
|                         }} | ||||
|                     /> | ||||
|                     <Tag | ||||
|                         tagInfo={{TagName: 'HD', TagId: DefaultTags.hd.TagId}} | ||||
|                         onclick={(): void => { | ||||
|                             this.tagState = DefaultTags.hd; | ||||
|                             this.fetchVideoData(); | ||||
|                             this.setState({subtitle: 'HD Videos'}); | ||||
|                         }} | ||||
|                     /> | ||||
|                 </SideBar> | ||||
|                 <div> | ||||
|                     <span className={style.sortbyLabel}>Sort By: </span> | ||||
|                     <div className={style.dropdown}> | ||||
|                         <span className={style.dropbtn}> | ||||
|                             <span>{this.state.sortby}</span> | ||||
|                             <FontAwesomeIcon style={{marginLeft: 3, paddingBottom: 3}} icon={faSortDown} size='1x' /> | ||||
|                         </span> | ||||
|                         <div className={style.dropdownContent}> | ||||
|                             <span onClick={(): void => this.onDropDownItemClick(SortBy.date, 'Date Added')}>Date Added</span> | ||||
|                             <span onClick={(): void => this.onDropDownItemClick(SortBy.likes, 'Most likes')}>Most likes</span> | ||||
|                             <span onClick={(): void => this.onDropDownItemClick(SortBy.random, 'Random')}>Random</span> | ||||
|                             <span onClick={(): void => this.onDropDownItemClick(SortBy.name, 'Name')}>Name</span> | ||||
|                             <span onClick={(): void => this.onDropDownItemClick(SortBy.length, 'Length')}>Length</span> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|  | ||||
|                 <VideoContainer data={this.state.data} /> | ||||
|                 <div className={style.rightinfo} /> | ||||
|                 <Switch> | ||||
|                     <Route path='/search/:name'> | ||||
|                         <SearchHandling /> | ||||
|                     </Route> | ||||
|                     <Route path='/'> | ||||
|                         <PageTitle title='Home Page' subtitle={this.state.subtitle + ' - ' + this.state.selectionnr}> | ||||
|                             <form | ||||
|                                 className={'form-inline ' + style.searchform} | ||||
|                                 onSubmit={(e): void => { | ||||
|                                     e.preventDefault(); | ||||
|                                     this.props.history.push('/search/' + this.keyword); | ||||
|                                 }}> | ||||
|                                 <input | ||||
|                                     data-testid='searchtextfield' | ||||
|                                     className='form-control mr-sm-2' | ||||
|                                     type='text' | ||||
|                                     placeholder='Search' | ||||
|                                     onChange={(e): void => { | ||||
|                                         this.keyword = e.target.value; | ||||
|                                     }} | ||||
|                                 /> | ||||
|                                 <button data-testid='searchbtnsubmit' className='btn btn-success' type='submit'> | ||||
|                                     Search | ||||
|                                 </button> | ||||
|                             </form> | ||||
|                         </PageTitle> | ||||
|                         <SideBar> | ||||
|                             <SideBarTitle>Infos:</SideBarTitle> | ||||
|                             <Line /> | ||||
|                             <SideBarItem> | ||||
|                                 <b>{this.state.sideinfo.VideoNr}</b> Videos Total! | ||||
|                             </SideBarItem> | ||||
|                             <SideBarItem> | ||||
|                                 <b>{this.state.sideinfo.FullHdNr}</b> FULL-HD Videos! | ||||
|                             </SideBarItem> | ||||
|                             <SideBarItem> | ||||
|                                 <b>{this.state.sideinfo.HDNr}</b> HD Videos! | ||||
|                             </SideBarItem> | ||||
|                             <SideBarItem> | ||||
|                                 <b>{this.state.sideinfo.SDNr}</b> SD Videos! | ||||
|                             </SideBarItem> | ||||
|                             <SideBarItem> | ||||
|                                 <b>{this.state.sideinfo.DifferentTags}</b> different Tags! | ||||
|                             </SideBarItem> | ||||
|                             <Line /> | ||||
|                             <SideBarTitle>Default Tags:</SideBarTitle> | ||||
|                             <Tag | ||||
|                                 tagInfo={{TagName: 'All', TagId: DefaultTags.all.TagId}} | ||||
|                                 onclick={(): void => { | ||||
|                                     this.fetchVideoData(DefaultTags.all.TagId); | ||||
|                                     this.setState({subtitle: 'All Videos'}); | ||||
|                                 }} | ||||
|                             /> | ||||
|                             <Tag | ||||
|                                 tagInfo={{TagName: 'Full Hd', TagId: DefaultTags.fullhd.TagId}} | ||||
|                                 onclick={(): void => { | ||||
|                                     this.fetchVideoData(DefaultTags.fullhd.TagId); | ||||
|                                     this.setState({subtitle: 'Full Hd Videos'}); | ||||
|                                 }} | ||||
|                             /> | ||||
|                             <Tag | ||||
|                                 tagInfo={{TagName: 'Low Quality', TagId: DefaultTags.lowq.TagId}} | ||||
|                                 onclick={(): void => { | ||||
|                                     this.fetchVideoData(DefaultTags.lowq.TagId); | ||||
|                                     this.setState({subtitle: 'Low Quality Videos'}); | ||||
|                                 }} | ||||
|                             /> | ||||
|                             <Tag | ||||
|                                 tagInfo={{TagName: 'HD', TagId: DefaultTags.hd.TagId}} | ||||
|                                 onclick={(): void => { | ||||
|                                     this.fetchVideoData(DefaultTags.hd.TagId); | ||||
|                                     this.setState({subtitle: 'HD Videos'}); | ||||
|                                 }} | ||||
|                             /> | ||||
|                         </SideBar> | ||||
|                         {this.state.data.length !== 0 ? <VideoContainer data={this.state.data} /> : <div>No Data found!</div>} | ||||
|                         <div className={style.rightinfo} /> | ||||
|                     </Route> | ||||
|                 </Switch> | ||||
|             </> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * click handler for sortby dropdown item click | ||||
|      * @param type type of sort action | ||||
|      * @param name new header title | ||||
|      */ | ||||
|     onDropDownItemClick(type: SortBy, name: string): void { | ||||
|         this.sortState = type; | ||||
|         this.setState({sortby: name}); | ||||
|         this.fetchVideoData(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default withRouter(HomePage); | ||||
|   | ||||
| @@ -57,7 +57,7 @@ export class SearchHandling extends React.Component<Props, state> { | ||||
|      * @param keyword The keyword to search for | ||||
|      */ | ||||
|     searchVideos(keyword: string): void { | ||||
|         callAPI(APINode.Video, {action: 'getSearchKeyWord', KeyWord: keyword}, (result: VideoTypes.VideoUnloadedType[]) => { | ||||
|         callAPI(APINode.Video, {action: 'getSearchKeyWord', keyword: keyword}, (result: VideoTypes.VideoUnloadedType[]) => { | ||||
|             this.setState({ | ||||
|                 data: result | ||||
|             }); | ||||
|   | ||||
| @@ -2,14 +2,12 @@ import {shallow} from 'enzyme'; | ||||
| import React from 'react'; | ||||
| import {Player} from './Player'; | ||||
| import {callAPI} from '../../utils/Api'; | ||||
| import GlobalInfos from "../../utils/GlobalInfos"; | ||||
| import {LoginContext} from '../../utils/context/LoginContext'; | ||||
|  | ||||
| describe('<Player/>', function () { | ||||
|  | ||||
|     // help simulating id passed by url | ||||
|     function instance() { | ||||
|         return shallow(<Player match={{params: {id: 10}}}/>, {context: LoginContext}); | ||||
|         return shallow(<Player match={{params: {id: 10}}}/>); | ||||
|     } | ||||
|  | ||||
|     it('renders without crashing ', function () { | ||||
| @@ -38,7 +36,7 @@ describe('<Player/>', function () { | ||||
|  | ||||
|         // initial fetch for getting movie data | ||||
|         expect(global.fetch).toHaveBeenCalledTimes(1); | ||||
|         wrapper.find('.videoactions').find('IconButton').first().simulate('click'); | ||||
|         wrapper.find('.videoactions').find('Button').first().simulate('click'); | ||||
|         // fetch for liking | ||||
|         expect(global.fetch).toHaveBeenCalledTimes(2); | ||||
|  | ||||
| @@ -81,49 +79,28 @@ describe('<Player/>', function () { | ||||
|         const wrapper = instance(); | ||||
|         expect(wrapper.find('AddTagPopup')).toHaveLength(0); | ||||
|         // todo dynamic button find without index | ||||
|         wrapper.find('.videoactions').find('IconButton').at(1).simulate('click'); | ||||
|         wrapper.find('.videoactions').find('Button').at(1).simulate('click'); | ||||
|         // addtagpopup should be showing now | ||||
|         expect(wrapper.find('AddTagPopup')).toHaveLength(1); | ||||
|     }); | ||||
|  | ||||
|     it('test fully delete popup rendering', function () { | ||||
|     it('test delete button', done => { | ||||
|         const wrapper = instance(); | ||||
|  | ||||
|         wrapper.setContext({VideosFullyDeleteable: true}) | ||||
|  | ||||
|         wrapper.setState({deletepopupvisible: true}); | ||||
|         wrapper.setProps({history: {goBack: jest.fn()}}); | ||||
|  | ||||
|         expect(wrapper.find('ButtonPopup')).toHaveLength(1) | ||||
|     }); | ||||
|         global.fetch = prepareFetchApi({result: 'success'}); | ||||
|  | ||||
|     it('test delete button', () => { | ||||
|         const wrapper = instance(); | ||||
|         const callback = jest.fn(); | ||||
|         wrapper.find('.videoactions').find('Button').at(2).simulate('click'); | ||||
|  | ||||
|         wrapper.setProps({history: {goBack: callback}}); | ||||
|         process.nextTick(() => { | ||||
|             // refetch is called so fetch called 3 times | ||||
|             expect(global.fetch).toHaveBeenCalledTimes(1); | ||||
|             expect(wrapper.instance().props.history.goBack).toHaveBeenCalledTimes(1); | ||||
|  | ||||
|         callAPIMock({result: 'success'}) | ||||
|         wrapper.setContext({VideosFullyDeleteable: false}) | ||||
|  | ||||
|         // request the popup to pop | ||||
|         wrapper.find('.videoactions').find('IconButton').at(2).simulate('click'); | ||||
|  | ||||
|         // click the first submit button | ||||
|         wrapper.find('ButtonPopup').dive().find('Button').at(0).simulate('click') | ||||
|  | ||||
|         // 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 | ||||
|         wrapper.setContext({VideosFullyDeleteable: 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); | ||||
|             global.fetch.mockClear(); | ||||
|             done(); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
| @@ -132,20 +109,7 @@ describe('<Player/>', function () { | ||||
|  | ||||
|         const func = jest.fn(); | ||||
|  | ||||
|         wrapper.setProps({history: {push: func}}); | ||||
|  | ||||
|         expect(func).toHaveBeenCalledTimes(0); | ||||
|         wrapper.find('.closebutton').simulate('click'); | ||||
|         // there shouldn't be a backstack available | ||||
|         expect(func).toHaveBeenCalledTimes(1); | ||||
|     }); | ||||
|  | ||||
|     it('hide click with stack available', function () { | ||||
|         const wrapper = instance(); | ||||
|  | ||||
|         const func = jest.fn(); | ||||
|  | ||||
|         wrapper.setProps({history: {goBack: func, length: 5}}); | ||||
|         wrapper.setProps({history: {goBack: func}}); | ||||
|  | ||||
|         expect(func).toHaveBeenCalledTimes(0); | ||||
|         wrapper.find('.closebutton').simulate('click'); | ||||
| @@ -175,14 +139,16 @@ describe('<Player/>', function () { | ||||
|     it('test click of quickadd tag btn', done => { | ||||
|         const wrapper = generatetag(); | ||||
|  | ||||
|         callAPIMock({result: 'success'}) | ||||
|         global.fetch = prepareFetchApi({result: 'success'}); | ||||
|  | ||||
|         // render tag subcomponent | ||||
|         const tag = wrapper.find('Tag').first().dive(); | ||||
|         tag.simulate('click'); | ||||
|  | ||||
|         process.nextTick(() => { | ||||
|             expect(callAPI).toHaveBeenCalledTimes(1); | ||||
|             expect(global.fetch).toHaveBeenCalledTimes(1); | ||||
|  | ||||
|             global.fetch.mockClear(); | ||||
|             done(); | ||||
|         }); | ||||
|     }); | ||||
| @@ -190,7 +156,7 @@ describe('<Player/>', function () { | ||||
|     it('test failing quickadd', done => { | ||||
|         const wrapper = generatetag(); | ||||
|  | ||||
|         callAPIMock({result: 'nonsuccess'}); | ||||
|         global.fetch = prepareFetchApi({result: 'nonsuccess'}); | ||||
|         global.console.error = jest.fn(); | ||||
|  | ||||
|         // render tag subcomponent | ||||
| @@ -199,6 +165,8 @@ describe('<Player/>', function () { | ||||
|  | ||||
|         process.nextTick(() => { | ||||
|             expect(global.console.error).toHaveBeenCalledTimes(2); | ||||
|  | ||||
|             global.fetch.mockClear(); | ||||
|             done(); | ||||
|         }); | ||||
|     }); | ||||
|   | ||||
| @@ -9,22 +9,20 @@ import Tag from '../../elements/Tag/Tag'; | ||||
| import AddTagPopup from '../../elements/Popups/AddTagPopup/AddTagPopup'; | ||||
| import PageTitle, {Line} from '../../elements/PageTitle/PageTitle'; | ||||
| import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; | ||||
| import {faPlusCircle, faTag, faThumbsUp, faTrash} from '@fortawesome/free-solid-svg-icons'; | ||||
| import {faPlusCircle} from '@fortawesome/free-solid-svg-icons'; | ||||
| import AddActorPopup from '../../elements/Popups/AddActorPopup/AddActorPopup'; | ||||
| import ActorTile from '../../elements/ActorTile/ActorTile'; | ||||
| import {withRouter} from 'react-router-dom'; | ||||
| import {APINode, callAPI} from '../../utils/Api'; | ||||
| import {RouteComponentProps} from 'react-router'; | ||||
| import {DefaultPlyrOptions, GeneralSuccess} from '../../types/GeneralTypes'; | ||||
| import {GeneralSuccess} from '../../types/GeneralTypes'; | ||||
| import {ActorType, TagType} from '../../types/VideoTypes'; | ||||
| import PlyrJS from 'plyr'; | ||||
| import {IconButton} from '../../elements/GPElements/Button'; | ||||
| import {Button} from '../../elements/GPElements/Button'; | ||||
| import {VideoTypes} from '../../types/ApiTypes'; | ||||
| import GlobalInfos from '../../utils/GlobalInfos'; | ||||
| import {ButtonPopup} from '../../elements/Popups/ButtonPopup/ButtonPopup'; | ||||
| import {FeatureContext} from '../../utils/context/FeatureContext'; | ||||
|  | ||||
| interface Props extends RouteComponentProps<{id: string}> {} | ||||
| interface myprops extends RouteComponentProps<{id: string}> {} | ||||
|  | ||||
| interface mystate { | ||||
|     sources?: PlyrJS.SourceInfo; | ||||
| @@ -32,13 +30,11 @@ interface mystate { | ||||
|     movieName: string; | ||||
|     likes: number; | ||||
|     quality: number; | ||||
|     releaseDate: string | null; | ||||
|     length: number; | ||||
|     tags: TagType[]; | ||||
|     suggesttag: TagType[]; | ||||
|     popupvisible: boolean; | ||||
|     actorpopupvisible: boolean; | ||||
|     deletepopupvisible: boolean; | ||||
|     actors: ActorType[]; | ||||
| } | ||||
|  | ||||
| @@ -46,8 +42,25 @@ interface mystate { | ||||
|  * Player page loads when a video is selected to play and handles the video view | ||||
|  * and actions such as tag adding and liking | ||||
|  */ | ||||
| export class Player extends React.Component<Props, mystate> { | ||||
|     constructor(props: Props) { | ||||
| export class Player extends React.Component<myprops, mystate> { | ||||
|     options: PlyrJS.Options = { | ||||
|         controls: [ | ||||
|             'play-large', // The large play button in the center | ||||
|             'play', // Play/pause playback | ||||
|             'progress', // The progress bar and scrubber for playback and buffering | ||||
|             'current-time', // The current time of playback | ||||
|             'duration', // The full duration of the media | ||||
|             'mute', // Toggle mute | ||||
|             'volume', // Volume control | ||||
|             'captions', // Toggle captions | ||||
|             'settings', // Settings menu | ||||
|             'airplay', // Airplay (currently Safari only) | ||||
|             'download', // Show a download button with a link to either the current source or a custom URL you specify in your options | ||||
|             'fullscreen' // Toggle fullscreen | ||||
|         ] | ||||
|     }; | ||||
|  | ||||
|     constructor(props: myprops) { | ||||
|         super(props); | ||||
|  | ||||
|         this.state = { | ||||
| @@ -55,21 +68,17 @@ export class Player extends React.Component<Props, mystate> { | ||||
|             movieName: '', | ||||
|             likes: 0, | ||||
|             quality: 0, | ||||
|             releaseDate: null, | ||||
|             length: 0, | ||||
|             tags: [], | ||||
|             suggesttag: [], | ||||
|             popupvisible: false, | ||||
|             actorpopupvisible: false, | ||||
|             deletepopupvisible: false, | ||||
|             actors: [] | ||||
|         }; | ||||
|  | ||||
|         this.quickAddTag = this.quickAddTag.bind(this); | ||||
|     } | ||||
|  | ||||
|     static contextType = FeatureContext; | ||||
|  | ||||
|     componentDidMount(): void { | ||||
|         // initial fetch of current movie data | ||||
|         this.fetchMovieData(); | ||||
| @@ -85,17 +94,23 @@ export class Player extends React.Component<Props, mystate> { | ||||
|                 <div className={style.videowrapper}> | ||||
|                     {/* video component is added here */} | ||||
|                     {this.state.sources ? ( | ||||
|                         <Plyr style={plyrstyle} source={this.state.sources} options={DefaultPlyrOptions} /> | ||||
|                         <Plyr style={plyrstyle} source={this.state.sources} options={this.options} /> | ||||
|                     ) : ( | ||||
|                         <div>not loaded yet</div> | ||||
|                     )} | ||||
|                     <div className={style.videoactions}> | ||||
|                         <IconButton icon={faThumbsUp} onClick={(): void => this.likebtn()} title='Like!' /> | ||||
|                         <IconButton icon={faTag} onClick={(): void => this.setState({popupvisible: true})} title='Add Tag!' /> | ||||
|                         <IconButton | ||||
|                             icon={faTrash} | ||||
|                             onClick={(): void => this.setState({deletepopupvisible: true})} | ||||
|                             title='Delete Video!' | ||||
|                         <Button onClick={(): void => this.likebtn()} title='Like this Video!' color={{backgroundColor: 'green'}} /> | ||||
|                         <Button | ||||
|                             onClick={(): void => this.setState({popupvisible: true})} | ||||
|                             title='Give this Video a Tag' | ||||
|                             color={{backgroundColor: '#3574fe'}} | ||||
|                         /> | ||||
|                         <Button | ||||
|                             title='Delete Video' | ||||
|                             onClick={(): void => { | ||||
|                                 this.deleteVideo(); | ||||
|                             }} | ||||
|                             color={{backgroundColor: 'red'}} | ||||
|                         /> | ||||
|                     </div> | ||||
|                     {this.assembleActorTiles()} | ||||
| @@ -127,11 +142,6 @@ export class Player extends React.Component<Props, mystate> { | ||||
|                         <b>{this.state.quality}p</b> Quality! | ||||
|                     </SideBarItem> | ||||
|                 ) : null} | ||||
|                 {this.state.releaseDate !== null ? ( | ||||
|                     <SideBarItem> | ||||
|                         <b>{this.state.releaseDate}</b> released! | ||||
|                     </SideBarItem> | ||||
|                 ) : null} | ||||
|                 {this.state.length !== 0 ? ( | ||||
|                     <SideBarItem> | ||||
|                         <b>{Math.round(this.state.length / 60)}</b> Minutes of length! | ||||
| @@ -203,46 +213,10 @@ export class Player extends React.Component<Props, mystate> { | ||||
|                         movieId={this.state.movieId} | ||||
|                     /> | ||||
|                 ) : null} | ||||
|                 {this.state.deletepopupvisible ? this.renderDeletePopup() : null} | ||||
|             </> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     renderDeletePopup(): JSX.Element { | ||||
|         if (this.context.VideosFullyDeleteable) { | ||||
|             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 | ||||
|      * @param tagId id of tag to add | ||||
| @@ -305,15 +279,17 @@ export class Player extends React.Component<Props, mystate> { | ||||
|             APINode.Video, | ||||
|             {action: 'loadVideo', MovieId: parseInt(this.props.match.params.id, 10)}, | ||||
|             (result: VideoTypes.loadVideoType) => { | ||||
|                 console.log(result); | ||||
|                 console.log(process.env.REACT_APP_CUST_BACK_DOMAIN); | ||||
|                 this.setState({ | ||||
|                     sources: { | ||||
|                         type: 'video', | ||||
|                         sources: [ | ||||
|                             { | ||||
|                                 src: | ||||
|                                     (process.env.REACT_APP_CUST_BACK_DOMAIN ? process.env.REACT_APP_CUST_BACK_DOMAIN : '') + | ||||
|                                     GlobalInfos.getVideoPath() + | ||||
|                                     result.MovieUrl, | ||||
|                                     (process.env.REACT_APP_CUST_BACK_DOMAIN | ||||
|                                         ? process.env.REACT_APP_CUST_BACK_DOMAIN | ||||
|                                         : GlobalInfos.getVideoPath()) + result.MovieUrl, | ||||
|                                 type: 'video/mp4', | ||||
|                                 size: 1080 | ||||
|                             } | ||||
| @@ -324,16 +300,11 @@ export class Player extends React.Component<Props, mystate> { | ||||
|                     movieName: result.MovieName, | ||||
|                     likes: result.Likes, | ||||
|                     quality: result.Quality, | ||||
|                     releaseDate: result.ReleaseDate, | ||||
|                     length: result.Length, | ||||
|                     tags: result.Tags, | ||||
|                     suggesttag: result.SuggestedTag, | ||||
|                     actors: result.Actors | ||||
|                 }); | ||||
|             }, | ||||
|             (_) => { | ||||
|                 // if there is an load error redirect to home page | ||||
|                 this.props.history.push('/'); | ||||
|             } | ||||
|         ); | ||||
|     } | ||||
| @@ -358,27 +329,23 @@ export class Player extends React.Component<Props, mystate> { | ||||
|      * calls callback to viewbinding to show previous page agains | ||||
|      */ | ||||
|     closebtn(): void { | ||||
|         const hist = this.props.history; | ||||
|         if (hist.length > 1) { | ||||
|             hist.goBack(); | ||||
|         } else { | ||||
|             hist.push('/'); | ||||
|         } | ||||
|         this.props.history.goBack(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * delete the current video and return to last page | ||||
|      */ | ||||
|     deleteVideo(fullyDelete: boolean): void { | ||||
|     deleteVideo(): void { | ||||
|         callAPI( | ||||
|             APINode.Video, | ||||
|             {action: 'deleteVideo', MovieId: parseInt(this.props.match.params.id, 10), FullyDelete: fullyDelete}, | ||||
|             {action: 'deleteVideo', MovieId: parseInt(this.props.match.params.id, 10)}, | ||||
|             (result: GeneralSuccess) => { | ||||
|                 if (result.result === 'success') { | ||||
|                     // return to last element if successful | ||||
|                     this.props.history.goBack(); | ||||
|                 } else { | ||||
|                     console.error('an error occured while deleting the video: ' + JSON.stringify(result)); | ||||
|                     console.error('an error occured while liking'); | ||||
|                     console.error(result); | ||||
|                 } | ||||
|             } | ||||
|         ); | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user