diff --git a/.eslintrc.js b/.eslintrc.js index 49f4aa6..52b1cbf 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -45,6 +45,8 @@ 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, diff --git a/apiGo/api/API.go b/apiGo/api/API.go index 020a7f1..94cf059 100644 --- a/apiGo/api/API.go +++ b/apiGo/api/API.go @@ -6,4 +6,5 @@ func AddHandlers() { addTagHandlers() addActorsHandlers() addTvshowHandlers() + addUploadHandler() } diff --git a/apiGo/api/FileUpload.go b/apiGo/api/FileUpload.go new file mode 100644 index 0000000..3f5c148 --- /dev/null +++ b/apiGo/api/FileUpload.go @@ -0,0 +1,70 @@ +package api + +import ( + "fmt" + "io" + "openmediacenter/apiGo/api/api" + "openmediacenter/apiGo/database" + "openmediacenter/apiGo/videoparser" + "os" + "path/filepath" +) + +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 + } + + // todo allow more video formats than mp4 + // only allow valid extensions + if filepath.Ext(part.FileName()) != ".mp4" { + 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"}) + }) +} diff --git a/apiGo/api/api/ApiBase.go b/apiGo/api/api/ApiBase.go index 375a4f0..a5ee382 100644 --- a/apiGo/api/api/ApiBase.go +++ b/apiGo/api/api/ApiBase.go @@ -15,7 +15,7 @@ const ( LoginNode = "login" ) -func AddHandler(action string, apiNode string, perm uint8, handler func(ctx Context)) { +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 { diff --git a/apiGo/api/api/Auth.go b/apiGo/api/api/Auth.go index 9e50ddd..982fde0 100644 --- a/apiGo/api/api/Auth.go +++ b/apiGo/api/api/Auth.go @@ -13,12 +13,18 @@ import ( var srv *server.Server +type Perm uint8 + const ( - PermAdmin uint8 = iota - PermUser uint8 = iota - PermUnauthorized uint8 = iota + PermAdmin Perm = iota + PermUser + PermUnauthorized ) +func (p Perm) String() string { + return [...]string{"PermAdmin", "PermUser", "PermUnauthorized"}[p] +} + const SignKey = "89013f1753a6890c6090b09e3c23ff43" const TokenExpireHours = 24 @@ -27,7 +33,7 @@ type Token struct { ExpiresAt int64 } -func TokenValid(token string) (int, uint8) { +func TokenValid(token string) (int, Perm) { t, err := jwt.ParseWithClaims(token, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) { return []byte(SignKey), nil }) @@ -42,7 +48,7 @@ func TokenValid(token string) (int, uint8) { if err != nil { return -1, PermUnauthorized } - return id, uint8(permid) + return id, Perm(permid) } func InitOAuth() { diff --git a/apiGo/api/api/Context.go b/apiGo/api/api/Context.go index edde0ef..3904fe5 100644 --- a/apiGo/api/api/Context.go +++ b/apiGo/api/api/Context.go @@ -13,6 +13,7 @@ type Context interface { GetRequest() *http.Request GetWriter() http.ResponseWriter UserID() int + PermID() Perm } type apicontext struct { @@ -20,7 +21,7 @@ type apicontext struct { request *http.Request responseWritten bool userid int - permid uint8 + permid Perm } func (r *apicontext) GetRequest() *http.Request { @@ -58,3 +59,7 @@ func (r *apicontext) Error(msg string) { func (r *apicontext) Errorf(msg string, args ...interface{}) { r.Error(fmt.Sprintf(msg, &args)) } + +func (r *apicontext) PermID() Perm { + return r.permid +} diff --git a/apiGo/videoparser/ReIndexVideos.go b/apiGo/videoparser/ReIndexVideos.go index 50f5626..1a59fa2 100644 --- a/apiGo/videoparser/ReIndexVideos.go +++ b/apiGo/videoparser/ReIndexVideos.go @@ -4,6 +4,7 @@ import ( "database/sql" "fmt" "openmediacenter/apiGo/api/types" + "openmediacenter/apiGo/config" "openmediacenter/apiGo/database" "openmediacenter/apiGo/videoparser/tmdb" "regexp" @@ -11,7 +12,7 @@ import ( "strings" ) -var mSettings types.SettingsType +var mSettings *types.SettingsType var mExtDepsAvailable *ExtDependencySupport // default Tag ids @@ -32,20 +33,22 @@ type VideoAttributes struct { Width uint } -func ReIndexVideos(path []string, sett types.SettingsType) { +func InitDeps(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) +} +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) + ProcessVideo(s) } AppendMessage("reindex finished successfully!") @@ -92,8 +95,8 @@ func filterExisting(paths []string) *[]string { return &resultarr } -func processVideo(fileNameOrig string) { - fmt.Printf("Processing %s video-", fileNameOrig) +func ProcessVideo(fileNameOrig string) { + fmt.Printf("Processing %s video\n", fileNameOrig) // match the file extension r := regexp.MustCompile(`\.[a-zA-Z0-9]+$`) @@ -120,8 +123,10 @@ func addVideo(videoName string, fileName string, year int) { Width: 0, } + vidFolder := config.GetConfig().General.ReindexPrefix + mSettings.VideoPath + if mExtDepsAvailable.FFMpeg { - ppic, err = parseFFmpegPic(mSettings.VideoPath + fileName) + ppic, err = parseFFmpegPic(vidFolder + fileName) if err != nil { fmt.Printf("FFmpeg error occured: %s\n", err.Error()) } else { @@ -130,7 +135,7 @@ func addVideo(videoName string, fileName string, year int) { } if mExtDepsAvailable.MediaInfo { - atr := getVideoAttributes(mSettings.VideoPath + fileName) + atr := getVideoAttributes(vidFolder + fileName) if atr != nil { vidAtr = atr } diff --git a/apiGo/videoparser/VideoParser.go b/apiGo/videoparser/VideoParser.go index 9175962..d64a01e 100644 --- a/apiGo/videoparser/VideoParser.go +++ b/apiGo/videoparser/VideoParser.go @@ -3,6 +3,7 @@ package videoparser import ( "fmt" "io/ioutil" + "openmediacenter/apiGo/config" "openmediacenter/apiGo/database" "os" "path/filepath" @@ -19,20 +20,21 @@ func StartReindex() bool { SendEvent("start") AppendMessage("starting reindex..") - mSettings, PathPrefix, _ := database.GetSettings() + mSettings, _, _ := database.GetSettings() + // add the path prefix to videopath - mSettings.VideoPath = PathPrefix + mSettings.VideoPath + vidFolder := config.GetConfig().General.ReindexPrefix + mSettings.VideoPath // check if path even exists - if _, err := os.Stat(mSettings.VideoPath); os.IsNotExist(err) { + if _, err := os.Stat(vidFolder); os.IsNotExist(err) { fmt.Println("Reindex path doesn't exist!") - AppendMessage(fmt.Sprintf("Reindex path doesn't exist! :%s", mSettings.VideoPath)) + AppendMessage(fmt.Sprintf("Reindex path doesn't exist! :%s", vidFolder)) SendEvent("stop") return false } var files []string - err := filepath.Walk(mSettings.VideoPath, func(path string, info os.FileInfo, err error) error { + err := filepath.Walk(vidFolder, func(path string, info os.FileInfo, err error) error { if err != nil { fmt.Println(err.Error()) return err @@ -49,7 +51,8 @@ func StartReindex() bool { } // start reindex process AppendMessage("Starting Reindexing!") - go ReIndexVideos(files, mSettings) + InitDeps(&mSettings) + go ReIndexVideos(files) return true } diff --git a/deb/OpenMediaCenter/etc/nginx/sites-available/OpenMediaCenter.conf b/deb/OpenMediaCenter/etc/nginx/sites-available/OpenMediaCenter.conf index cbdb815..ce85ae1 100755 --- a/deb/OpenMediaCenter/etc/nginx/sites-available/OpenMediaCenter.conf +++ b/deb/OpenMediaCenter/etc/nginx/sites-available/OpenMediaCenter.conf @@ -13,7 +13,8 @@ server { try_files $uri /index.html; } - location ~* ^/(api/|token) { + location ~* ^/(api) { + client_max_body_size 10G; proxy_pass http://127.0.0.1:8081; } location /subscribe { diff --git a/src/elements/DropZone/DropZone.module.css b/src/elements/DropZone/DropZone.module.css new file mode 100644 index 0000000..f578e61 --- /dev/null +++ b/src/elements/DropZone/DropZone.module.css @@ -0,0 +1,30 @@ +.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; +} diff --git a/src/elements/DropZone/DropZone.tsx b/src/elements/DropZone/DropZone.tsx new file mode 100644 index 0000000..ba1b53c --- /dev/null +++ b/src/elements/DropZone/DropZone.tsx @@ -0,0 +1,108 @@ +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(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 ( +
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(); + }}> +
+

To upload new Videos darg and drop them here or click to select some...

+
+
+
+ {finished !== null ? ( + finished === '' ? ( +
Finished uploading
+ ) : ( +
Upload failed: {finished}
+ ) + ) : ( + <> + )} +
+
+ ); +}; diff --git a/src/pages/SettingsPage/MovieSettings.module.css b/src/pages/SettingsPage/MovieSettings.module.css index 37c5ef8..e67147d 100644 --- a/src/pages/SettingsPage/MovieSettings.module.css +++ b/src/pages/SettingsPage/MovieSettings.module.css @@ -11,3 +11,9 @@ padding: 10px; width: 40%; } + +.uploadtext { + font-size: x-large; + margin-top: 30px; + margin-bottom: 10px; +} diff --git a/src/pages/SettingsPage/MovieSettings.tsx b/src/pages/SettingsPage/MovieSettings.tsx index 52262e6..be9ce10 100644 --- a/src/pages/SettingsPage/MovieSettings.tsx +++ b/src/pages/SettingsPage/MovieSettings.tsx @@ -2,6 +2,8 @@ import React from 'react'; import style from './MovieSettings.module.css'; import {APINode, callAPI} from '../../utils/Api'; import {GeneralSuccess} from '../../types/GeneralTypes'; +import {DropZone} from '../../elements/DropZone/DropZone'; +import GlobalInfos from '../../utils/GlobalInfos'; interface state { text: string[]; @@ -99,6 +101,8 @@ class MovieSettings extends React.Component { } render(): JSX.Element { + const theme = GlobalInfos.getThemeStyle(); + return ( <>
))} +
+
Video Upload:
+ +
); }