Compare commits
108 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
824866bdd8
|
|||
|
0b76ea08cd
|
|||
|
ba1e96db26
|
|||
|
ff85724982
|
|||
|
ad388cf83b
|
|||
|
881334cccb
|
|||
|
edc42248ee
|
|||
|
d7b6b3849c
|
|||
|
4fcd962cc9
|
|||
|
c7382a1561
|
|||
|
4199caa5ef
|
|||
|
1cf8df7524
|
|||
|
9ea21fa7f6
|
|||
|
8859640411
|
|||
|
d07df60b5d
|
|||
|
02e5a39814
|
|||
|
522844a447
|
|||
|
32521e1178
|
|||
|
9f4c606b28
|
|||
|
58bb37fede
|
|||
|
ff9774b806
|
|||
|
7cb1a5d02f
|
|||
|
a9c2c2d7f9
|
|||
|
212ab56722
|
|||
|
c2269ac0fc
|
|||
|
14e9ff5a95
|
|||
|
544b5afc0d
|
|||
|
6e9e7252b2
|
|||
|
61d85bba97
|
|||
|
e4c59fd539
|
|||
|
25205c0c0d
|
|||
|
1655962e85
|
|||
|
2d85244ced
|
|||
|
0897fcc9f8
|
|||
|
2dab5cd109
|
|||
|
a84dfe8178
|
|||
|
a367b9253e
|
|||
|
b295707a05
|
|||
|
82da1aa48b
|
|||
|
359416ea44
|
|||
|
5608693c06
|
|||
|
d6f9a03786
|
|||
|
7188e73783
|
|||
|
027aef4070
|
|||
|
14f3c1c8da
|
|||
|
80802f95f8
|
|||
|
214ad6c285
|
|||
|
64d1e3ff78
|
|||
|
297643ad91
|
|||
|
7efde3cf6f
|
|||
|
b0e57099ba
|
|||
|
435e3605f7
|
|||
|
0edc7f7141
|
|||
|
6fdd0d2a9e
|
|||
|
465517e5cc
|
|||
|
fc735c88d3
|
|||
|
82c198d45b
|
|||
|
73828c547c
|
|||
|
97083114fb
|
|||
|
1c14997b85
|
|||
|
d59e40c6fa
|
|||
|
47b178ac90
|
|||
|
922b2370ee
|
|||
|
83ab1a91b2
|
|||
|
4bfcf460c9
|
|||
|
28127f6138
|
|||
|
0c9048072f
|
|||
|
22f7ea8476
|
|||
|
e1d9eac514
|
|||
|
1b0688e523
|
|||
|
f1e8dcfad4
|
|||
|
ab67575976
|
|||
|
adee9bcb65
|
|||
|
d7ca68aad1
|
|||
|
a826e4bf29
|
|||
|
d4718ac120
|
|||
|
7a10922ec4
|
|||
|
164dd0f282
|
|||
|
f32c922faf
|
|||
|
80462efebc
|
|||
|
12739be2f5
|
|||
|
6b8bfedb9b
|
|||
|
e87b4823d9
|
|||
|
a2cb098330
|
|||
|
93a0fe7a31
|
|||
|
003f8cace2
|
|||
|
2c802ca807
|
|||
|
f71544caab
|
|||
|
dfc0b43bdd
|
|||
|
5a68cea2f3
|
|||
|
047f15426b
|
|||
|
d2c852d57a
|
|||
|
af444f0bbb
|
|||
|
1bbcecfb2e
|
|||
|
b96daa07c6
|
|||
|
1f960f8f64
|
|||
|
e608a6b1df
|
|||
|
f3a95973e9
|
|||
|
c580ca245f
|
|||
|
05a569e395
|
|||
|
e961c10d4e
|
|||
|
2d7ac69db5
|
|||
|
47a60ae671
|
|||
|
ca8b6cb893
|
|||
|
258bf9869f
|
|||
|
be2515231c
|
|||
|
2358335d4e
|
|||
|
1bef4d0272
|
4
Makefile
4
Makefile
@@ -3,7 +3,7 @@ dist:
|
||||
cd web && npm run build
|
||||
|
||||
linux:
|
||||
go build
|
||||
go build -v -ldflags '-linkmode=external -extldflags=-static' -tags sqlite_omit_load_extension,netgo
|
||||
|
||||
windows:
|
||||
CC=x86_64-w64-mingw32-gcc CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build
|
||||
CC=x86_64-w64-mingw32-gcc CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -v
|
||||
|
||||
387
README.md
387
README.md
@@ -1,74 +1,97 @@
|
||||
# MSW Open Music Project
|
||||
|
||||
> The best way to search for a music is to load up a huge playlist and shuffle until you find it.
|
||||
|
||||
A 💪 light weight ⚡️ blazingly fast 🖥️ cross platform personal music streaming platform. Manage your existing music files and enjoy them on any devices.
|
||||
|
||||
Front-end web application build with `react.js` and `water.css`, back-end build with `golang` and `sqlite`.
|
||||
|
||||
## Introduction
|
||||
|
||||
A light weight personal music streaming platform.
|
||||
Screenshot
|
||||
|
||||

|
||||
|
||||
[toc]
|
||||
### Features
|
||||
|
||||
## How to build
|
||||
- 🔎 Index your existing music files, and record file name and folder information.
|
||||
|
||||
### Build the back-end server
|
||||
- 📕 Use folder 📁 tag 🏷️ review 💬 to manage your music.
|
||||
|
||||
`make linux` or `make windows`
|
||||
- 🌐 Provide a light weight web application with multi-language support.
|
||||
|
||||
The executable file is named `msw-open-music` or `msw-open-music.exe`
|
||||
- 👥 Multi-user support.
|
||||
|
||||
### Build the font-end web pages
|
||||
- 🔥 Call `ffmpeg` with customizable preset to stream your music.
|
||||
|
||||
To build production web page `make web`
|
||||
- 🔗 Share music with others!
|
||||
|
||||
This command will go into `web` directory and install `node_modules`. Then execute `npm run build` command. The built web pages is under `web/build` directory.
|
||||
### Try it if you...
|
||||
|
||||
To start the development, run `cd web` and `npm start`
|
||||
- Already saved a lot of music files on disk. 🖴
|
||||
|
||||
- Downloaded tons of huge lossless music. 🎵
|
||||
|
||||
- Wants to stream your music files from PC/Server to PC/phone. 😋
|
||||
|
||||
- Wants to share your stored music. 😘
|
||||
|
||||
## Usage
|
||||
|
||||
Start back-end server. Server will listen on 8080 port.
|
||||
1. Modify the `secret` in `config.json`
|
||||
|
||||
Build the font-end web page, then go to <http://127.0.0.1:8080>
|
||||
2. Run back-end server `msw-open-music.exe` or `msw-open-music`. Server will listen on 8080 port by default. Then open <http://127.0.0.1:8080> to setup first admin account.
|
||||
|
||||
By default:
|
||||
The front-end HTML files are under `web/build`
|
||||
|
||||
- URL matched `/api/*` will process by back-end server.
|
||||
- Others URL matched `/*` will be served files under `web/build/`
|
||||
### Setup first admin account
|
||||
|
||||
### Run back-end server
|
||||
The first administrator account will be active automatically, other administrator accounts need active manually.
|
||||
|
||||
Configuration file is `config.json`, **Please modify your `token`** 。
|
||||
Go to register page, select the role to admin, and register the first admin account.
|
||||
|
||||
Default `ffmpeg_threads` is 1. Seems value larger than 1 will not increase the audio encode speed.
|
||||
#### config.json
|
||||
|
||||
#### config.json description
|
||||
- `secret` string type. Secret to encrypt the session.
|
||||
|
||||
- `database_name` string type. The filename of `sqlite3` database. Will create if that file doesn't exist.
|
||||
- `addr` string type. The listen address and port.
|
||||
- `token` string type. Password.
|
||||
- `ffmpeg_config_list` list type, include `ffmpegConfig` object.
|
||||
- `file_life_time` integer type (second). Life time for temporary file. If the temporary file is not accessed for more than this time, back-end server will delete this file.
|
||||
- `cleaner_internal` integer type (second). Interval for `tmpfs` checking temporary file.
|
||||
- `root` string type. Directory to store temporary files. Default is `/tmp`, **please modify this directory if you are using Windows.**
|
||||
- `root` string type. Directory to store temporary files. Default is `/tmp`, **please modify this directory if you are using Windows.** Directory will be created if not exists.
|
||||
|
||||
### Run font-end web page
|
||||
For windows user, make sure you have `ffmpeg` installed.
|
||||
|
||||
Open your web browser to <http://127.0.0.1:8080> you will see the web pages.
|
||||
## Development
|
||||
|
||||
## About tmpfs
|
||||
Any issues or pull requests are welcome.
|
||||
|
||||
If the `Prepare` mode is enabled in the font-wed player, back-end server will convert the whole file into the temporary folder, then serve file using native method. This can avoid ffmpeg pipe break problem cause by unstable network connection while streaming audio.
|
||||
### Major changes log
|
||||
|
||||
- `v1.0.0` First version. Implement the core streaming function.
|
||||
- `v1.1.0` Use `React` to rewrite the font-end web pages.
|
||||
- `v1.2.0` Add user, tag, review and other functions for DBMS course project.
|
||||
|
||||
### ER Diagram
|
||||
|
||||
Database Entities Relationship Diagram
|
||||
|
||||

|
||||
|
||||
- `avatar` is not using currently
|
||||
|
||||
- The first time you run the program, the server will create an anonymous user with id `1`. All users who are not logged in will be automatically logged in to this account.
|
||||
|
||||
- `tmpfs` is store in memory, which will be empty everytime server restart.
|
||||
|
||||
### About tmpfs
|
||||
|
||||
If the `Prepare` mode is enabled in the font-wed player, back-end server will convert the whole file into the temporary folder, then serve file. This can avoid `ffmpeg` pipe break problem cause by unstable network connection while streaming audio.
|
||||
|
||||
The default temporary folder is `/tmp`, which is a `tmpfs` file system in Linux operating system. Default life time for temporary files is 600 seconds (10 minutes). If the temporary file is not accessed for more than this time, back-end server will delete this file.
|
||||
|
||||
## Change log
|
||||
|
||||
- `v1.0.0` First version. Ready to use in production environment.
|
||||
- `v1.1.0` Use `React` to rewrite the font-end web pages (Previous using `Vue`).
|
||||
|
||||
## Back-end API references
|
||||
|
||||
API named `stream` means it transfer data using `io.Copy`, which **DO NOT** support continue getting a partially-downloaded audio.
|
||||
### Back-end API design
|
||||
|
||||
API does not need to respond any data will return the following JSON object.
|
||||
|
||||
@@ -78,296 +101,22 @@ API does not need to respond any data will return the following JSON object.
|
||||
}
|
||||
```
|
||||
|
||||
### Anonymous API
|
||||
Sometime errors happen, server will return the following JSON object, which `error` is the detailed error message.
|
||||
|
||||
Anonymous API can be called by anonymous.
|
||||
```json
|
||||
{
|
||||
"error": "Wrong password"
|
||||
}
|
||||
```
|
||||
|
||||
- `/api/v1/hello` Just for test purpose.
|
||||
API does not need to send any data should use `GET` method, otherwise use `POST` method.
|
||||
|
||||
- `/api/v1/get_file` Get a file with `stream` mode.
|
||||
Server use cookies to authenticate a user. Any request without cookies will be consider from an anonymous user (aka. user with ID `1`).
|
||||
|
||||
- Request example
|
||||
Some important source code files:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 123
|
||||
}
|
||||
```
|
||||
- `pkg/api/api.go` define URL
|
||||
|
||||
- `/api/v1/get_file_direct` Get a file with standart `http` methods, implement by `http.ServeFile` method.
|
||||
- `pkg/database/sql_stmt.go` define SQL queries and do the init job.
|
||||
|
||||
- Request example
|
||||
|
||||
`/api/v1/get_file_direct?id=30`
|
||||
|
||||
- `/api/v1/search_files` Search files by filename.
|
||||
|
||||
- Request example
|
||||
|
||||
```json
|
||||
{
|
||||
"filename": "miku",
|
||||
"limit": 10,
|
||||
"offset" 0
|
||||
}
|
||||
```
|
||||
|
||||
Search all files' name like `%miku%`. `%` is the wildcard in SQL. For example, `"filename": "miku%hatsune"` can match `hatsune miku`.
|
||||
|
||||
`limit` Numbers of files in the respond. Should be within 1 - 10;
|
||||
|
||||
`offset` It is the offset of the result, related to the page turning function.
|
||||
|
||||
- Respond example
|
||||
|
||||
```json
|
||||
{
|
||||
"files": [
|
||||
{
|
||||
"id": 30,
|
||||
"folder_id": 100,
|
||||
"folder_name": "wonderful",
|
||||
"filename": "memories.flac",
|
||||
"filesize": 1048576
|
||||
},
|
||||
{
|
||||
"id": 31,
|
||||
"folder_id": 100,
|
||||
"folder_name": "wonderful",
|
||||
"filename": "memories (instrunment).flac",
|
||||
"filesize": 1248531
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`id` Identification of file.
|
||||
|
||||
`folder_id` Identification of folder.
|
||||
|
||||
`foldername` Folder name where the file in.
|
||||
|
||||
`filename` File name.
|
||||
|
||||
`filesize` File size, unit is byte.
|
||||
|
||||
- `/api/v1/search_folders` Search folders.
|
||||
|
||||
- Request example.
|
||||
|
||||
```json
|
||||
{
|
||||
"foldername": "miku",
|
||||
"limit": 10,
|
||||
"offset": 0,
|
||||
}
|
||||
```
|
||||
|
||||
Search all folders' name like `%miku%`. `%` is the wildcard in SQL. For example, `"filename": "miku%hatsune"` can match `hatsune miku`.
|
||||
|
||||
`limit` Numbers of files in the respond. Should be within 1 - 10;
|
||||
|
||||
`offset` It is the offset of the result, related to the page turning function.
|
||||
|
||||
- Respond example
|
||||
|
||||
```json
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"id": 100,
|
||||
"foldername": "folder name"
|
||||
},
|
||||
{
|
||||
"id": 101,
|
||||
"foldername": "folder name (another)"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`id` Identification of folder.
|
||||
|
||||
`foldername` Folder name.
|
||||
|
||||
- `/api/v1/get_files_in_folder` Get files in a specify folder.
|
||||
|
||||
- Request example.
|
||||
|
||||
```json
|
||||
{
|
||||
"folder_id": 123,
|
||||
"limit": 10,
|
||||
"offset": 0
|
||||
}
|
||||
```
|
||||
|
||||
- Respond example.
|
||||
|
||||
Same with `/api/v1/search_files`
|
||||
|
||||
- `/api/v1/get_random_files` Randomly get 10 files.
|
||||
|
||||
- Request example.
|
||||
|
||||
GET `/api/v1/get_random_files`
|
||||
|
||||
- Respond example.
|
||||
|
||||
Same with `/api/v1/search_files`
|
||||
|
||||
- `/api/v1/get_file_stream`
|
||||
|
||||
Stream file with a ffmpeg config name.
|
||||
|
||||
- Request example.
|
||||
|
||||
GET `/api/v1/get_file_stream?id=123&config=OPUS%20128k`
|
||||
|
||||
- `/api/v1/get_ffmpeg_config_list`
|
||||
|
||||
Get ffmpeg config list
|
||||
|
||||
- Request example
|
||||
|
||||
GET `/api/v1/get_ffmpeg_config_list`
|
||||
|
||||
- Respond example
|
||||
|
||||
```json
|
||||
{
|
||||
"ffmpeg_config_list": [
|
||||
{"name": "OPUS 256k", "args": "-c:a libopus -ab 256k"},
|
||||
{"name": "WAV", "args": "-c:a wav"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `/api/v1/feedback` Send a feedback.
|
||||
|
||||
- Request example
|
||||
|
||||
```json
|
||||
{
|
||||
"feedback": "some suggestions..."
|
||||
}
|
||||
```
|
||||
|
||||
- Respond OK.
|
||||
|
||||
- `/api/v1/get_file_info` Get information of a specify file.
|
||||
|
||||
- Request example.
|
||||
|
||||
```json
|
||||
{
|
||||
"ID": 123
|
||||
}
|
||||
```
|
||||
|
||||
- Respond example.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 30,
|
||||
"folder_id": 100,
|
||||
"folder_name": "wonderful",
|
||||
"filename": "memories.flac",
|
||||
"filesize": 1048576
|
||||
},
|
||||
```
|
||||
|
||||
- `/api/v1/get_file_stream_direct` Get a ffmpeg converted file with native http method. This API support continue getting a partially-downloaded audio. Note, you should call `/api/v1/prepare_file_stream_direct` first and wait for its respond, then call this API.
|
||||
|
||||
- Request example
|
||||
|
||||
GET `/api/v1/get_file_stream_direct?id=123&config=OPUS%20128k`
|
||||
|
||||
- `/api/v1/prepare_file_stream_direct` Ask server to convert a file with specific ffmpeg config name. When the conver process is finished, server will reply with the converted file size.
|
||||
|
||||
- Request example
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 123,
|
||||
"config_name": "OPUS 128k"
|
||||
}
|
||||
```
|
||||
|
||||
- Respond example
|
||||
|
||||
```json
|
||||
{
|
||||
"filesize": 1973241
|
||||
}
|
||||
```
|
||||
|
||||
### API needs token
|
||||
|
||||
- `/api/v1/walk` Walk directory, add all files and folders to database.
|
||||
|
||||
- Request example
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "your token",
|
||||
"root": "/path/to/root",
|
||||
"pattern": [".wav", ".flac"]
|
||||
}
|
||||
```
|
||||
|
||||
`token` The token in `config.json` file.
|
||||
|
||||
`root` Root directory server will walk throught
|
||||
|
||||
`pattern` A list of pattern that files ends with. Only files matched a pattern in list will be add to database.
|
||||
|
||||
- Respond OK
|
||||
|
||||
- `/api/v1/reset` Rest the **files and folders table**
|
||||
|
||||
- Request example
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "your token"
|
||||
}
|
||||
```
|
||||
|
||||
- Respond OK
|
||||
|
||||
- `/api/v1/add_ffmpeg_config` Add ffmpeg config.
|
||||
|
||||
Will be changed in future.
|
||||
|
||||
- Request example
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "your token",
|
||||
"name": "OPUS",
|
||||
"ffmpeg_config": {
|
||||
"args": "-c:a libopus -ab 256k"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`name` Name of the ffmpeg config.
|
||||
|
||||
`ffmpeg_config`
|
||||
|
||||
`args`
|
||||
|
||||
- Respond OK
|
||||
|
||||
## Font-end API references
|
||||
|
||||
Currently only few APIs in font-end.
|
||||
|
||||
- `/#/files/39/share`
|
||||
|
||||
Share a specific file.
|
||||
|
||||
- `/#/folders/2614`
|
||||
|
||||
Show files in a specific folder.
|
||||
- `pkg/database/struct.go` define JSON structures for database entities.
|
||||
|
||||
44
config.json
44
config.json
@@ -1,18 +1,44 @@
|
||||
{
|
||||
"api": {
|
||||
"secret": "CHANGE_YOUR_SECRET_HERE",
|
||||
"database_name": "music.sqlite3",
|
||||
"single_thread": true,
|
||||
"addr": ":8080",
|
||||
"token": "!! config your very strong token here !!",
|
||||
"ffmpeg_threads": 1,
|
||||
"ffmpeg_config_list": [
|
||||
{"name": "OPUS 128k", "args": "-c:a libopus -ab 128k"},
|
||||
{"name": "OPUS 96k", "args": "-c:a libopus -ab 96k"},
|
||||
{"name": "OPUS 256k", "args": "-c:a libopus -ab 256k"},
|
||||
{"name": "OPUS 320k", "args": "-c:a libopus -ab 320k"},
|
||||
{"name": "OPUS 512k", "args": "-c:a libopus -ab 512k"},
|
||||
{"name": "AAC 128k", "args": "-c:a aac -ab 128k"},
|
||||
{"name": "AAC 256k", "args": "-c:a aac -ab 256k"},
|
||||
{"name": "全损音质 32k", "args": "-c:a libopus -ab 32k"}
|
||||
{
|
||||
"name": "WEBM OPUS 128k",
|
||||
"args": "-c:a libopus -ab 128k -vn",
|
||||
"format": "webm"
|
||||
},
|
||||
{
|
||||
"name": "WEBM OPUS 96k",
|
||||
"args": "-c:a libopus -ab 96k -vn",
|
||||
"format": "webm"
|
||||
},
|
||||
{
|
||||
"name": "WEBM OPUS 256k",
|
||||
"args": "-c:a libopus -ab 256k -vn",
|
||||
"format": "webm"
|
||||
},
|
||||
{
|
||||
"name": "WEBM OPUS 512k",
|
||||
"args": "-c:a libopus -ab 512k -vn",
|
||||
"format": "webm"
|
||||
},
|
||||
{
|
||||
"name": "AAC 128k",
|
||||
"args": "-c:a aac -ab 128k -vn",
|
||||
"format": "adts"
|
||||
},
|
||||
{
|
||||
"name": "AAC 256k",
|
||||
"args": "-c:a aac -ab 256k -vn",
|
||||
"format": "adts"
|
||||
},
|
||||
{ "name": "MP3 128k", "args": "-c:a mp3 -ab 128k -vn", "format": "mp3" },
|
||||
{ "name": "MP3 320k", "args": "-c:a mp3 -ab 320k -vn", "format": "mp3" },
|
||||
{ "name": "全损音质 8k", "args": "-c:a libopus -ab 8k -vn", "format": "webm" }
|
||||
]
|
||||
},
|
||||
"tmpfs": {
|
||||
|
||||
1
docs/ER Diagram.drawio
Normal file
1
docs/ER Diagram.drawio
Normal file
File diff suppressed because one or more lines are too long
36
docs/problem_description.md
Normal file
36
docs/problem_description.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# DBMS Group Project Problem Description
|
||||
|
||||
- Group 1
|
||||
|
||||
The Internet infrastructure construction has made the network speed development faster. With the fast Internet, people are gradually migrating various data and services to the cloud. For example, NetEase Cloud Music, Spotify, and Apple Music, we call them streaming media platforms. The definition of streaming media platform is that users purchase the digital copyright of music and then play the music online on the platform.
|
||||
|
||||
Generally speaking, users cannot buy music that is not available on the platform. The user cannot download the digital file of the music (the user purchases the right to play instead of the right to copy). Users cannot upload their music to the platform.
|
||||
|
||||
However, in the era of digital copyright, there are still many advantages to getting original music files, such as no need to install a dedicated player; free copying to other devices (without violating copyright); no risk of music unavailable from the platform; no play records and privacy will be tracked by the platform.
|
||||
|
||||
Some people don't like streaming platforms. They like to collect music (download or buy CDs) and save it on their computers. But as more and more music is collected (over 70,000 songs and in total size of 800GB), it becomes very difficult to manage files. It is difficult for them to find where the songs they want to listen to are saved. Also, lossless music files are large and difficult to play online.
|
||||
|
||||
As long as there no such "Self-hosted music streaming platform" software available, we decided to develop a project based on database knowledge to help people who have collected a lot of music to enjoy their music simply.
|
||||
|
||||
We will handle various relevant types of data in our database. Including song name, album name, file size, update date, rating, comment, user information, etc. They are highly relevant, so using a relational database will be a good choice.
|
||||
|
||||
The features of the project we designed are as follows:
|
||||
|
||||
- Open. Independent front-end (GUI) and back-end (server program), using API to communicate.
|
||||
- Easy to use. Minimize dependencies, allowing users to configure quickly and simply.
|
||||
- Lightweight. The program is small in size and quick to install.
|
||||
- High performance. Only do what should be done, no features that will lead to poor performance.
|
||||
- Cross-platform. The project can run on computers, mobile phones, Linux, Windows, macOS, and X86 and ARM processor architectures.
|
||||
- Extensibility. Access to cloud OSS (Object Storage Service), reverse proxy, or other external software.
|
||||
|
||||
Our project has the following functions:
|
||||
|
||||
- Index file. Index local files into the database.
|
||||
- Search. Search for music based on name/album/tag/comment, sorted by rating or other columns.
|
||||
- Play. Play music online, play music randomly and play music at a low bit rate on a bad network.
|
||||
- User management. Users can register and log in.
|
||||
- Comment. Users can give a like or comment on the music.
|
||||
- Management. The administrator can upload music, update or delete the database.
|
||||
- Share. Generate a link to share the music with others.
|
||||
|
||||
After research and discussion, in order to meet the above requirements, we decided to use the Golang programming language on the backend. SQLite as a database program. Vue as the front-end GUI interface.
|
||||
BIN
erdiagram.png
Normal file
BIN
erdiagram.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
10
go.mod
10
go.mod
@@ -1,5 +1,11 @@
|
||||
module msw-open-music
|
||||
|
||||
go 1.16
|
||||
go 1.18
|
||||
|
||||
require github.com/mattn/go-sqlite3 v1.14.7 // indirect
|
||||
require (
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/mattn/go-sqlite3 v1.14.14
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
|
||||
)
|
||||
|
||||
require github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
|
||||
10
go.sum
10
go.sum
@@ -1,2 +1,8 @@
|
||||
github.com/mattn/go-sqlite3 v1.14.7 h1:fxWBnXkxfM6sRiuH3bqJ4CfzZojMOLVc0UTsTglEghA=
|
||||
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/mattn/go-sqlite3 v1.14.14 h1:qZgc/Rwetq+MtyE18WhzjokPD93dNqLGNT3QJuLvBGw=
|
||||
github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
|
||||
5
main.go
5
main.go
@@ -5,6 +5,7 @@ import (
|
||||
"flag"
|
||||
"log"
|
||||
"msw-open-music/pkg/api"
|
||||
"msw-open-music/pkg/commonconfig"
|
||||
"os"
|
||||
)
|
||||
|
||||
@@ -14,12 +15,11 @@ func init() {
|
||||
flag.StringVar(&ConfigFilePath, "config", "config.json", "backend config file path")
|
||||
}
|
||||
|
||||
|
||||
func main() {
|
||||
var err error
|
||||
flag.Parse()
|
||||
|
||||
config := api.Config{}
|
||||
config := commonconfig.Config{}
|
||||
configFile, err := os.Open(ConfigFilePath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -37,7 +37,6 @@ func main() {
|
||||
log.Println("Starting",
|
||||
config.APIConfig.DatabaseName,
|
||||
config.APIConfig.Addr,
|
||||
config.APIConfig.Token,
|
||||
)
|
||||
log.Fatal(api.Server.ListenAndServe())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gorilla/sessions"
|
||||
"msw-open-music/pkg/commonconfig"
|
||||
"msw-open-music/pkg/database"
|
||||
"msw-open-music/pkg/tmpfs"
|
||||
"net/http"
|
||||
@@ -9,40 +11,25 @@ import (
|
||||
type API struct {
|
||||
Db *database.Database
|
||||
Server http.Server
|
||||
token string
|
||||
APIConfig APIConfig
|
||||
APIConfig commonconfig.APIConfig
|
||||
Tmpfs *tmpfs.Tmpfs
|
||||
store *sessions.CookieStore
|
||||
defaultSessionName string
|
||||
}
|
||||
|
||||
func NewAPIConfig() APIConfig {
|
||||
apiConfig := APIConfig{}
|
||||
return apiConfig
|
||||
}
|
||||
|
||||
type APIConfig struct {
|
||||
DatabaseName string `json:"database_name"`
|
||||
Addr string `json:"addr"`
|
||||
Token string `json:"token"`
|
||||
FfmpegThreads int64 `json:"ffmpeg_threads"`
|
||||
FfmpegConfigList []FfmpegConfig `json:"ffmpeg_config_list"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
APIConfig APIConfig `json:"api"`
|
||||
TmpfsConfig tmpfs.TmpfsConfig `json:"tmpfs"`
|
||||
}
|
||||
|
||||
func NewAPI(config Config) (*API, error) {
|
||||
func NewAPI(config commonconfig.Config) (*API, error) {
|
||||
var err error
|
||||
|
||||
apiConfig := config.APIConfig
|
||||
tmpfsConfig := config.TmpfsConfig
|
||||
|
||||
db, err := database.NewDatabase(apiConfig.DatabaseName)
|
||||
db, err := database.NewDatabase(apiConfig.DatabaseName, apiConfig.SingleThread)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
store := sessions.NewCookieStore([]byte(config.APIConfig.SECRET))
|
||||
|
||||
mux := http.NewServeMux()
|
||||
apiMux := http.NewServeMux()
|
||||
|
||||
@@ -53,6 +40,8 @@ func NewAPI(config Config) (*API, error) {
|
||||
Handler: mux,
|
||||
},
|
||||
APIConfig: apiConfig,
|
||||
store: store,
|
||||
defaultSessionName: "msw-open-music",
|
||||
}
|
||||
api.Tmpfs = tmpfs.NewTmpfs(tmpfsConfig)
|
||||
|
||||
@@ -64,21 +53,53 @@ func NewAPI(config Config) (*API, error) {
|
||||
apiMux.HandleFunc("/search_folders", api.HandleSearchFolders)
|
||||
apiMux.HandleFunc("/get_files_in_folder", api.HandleGetFilesInFolder)
|
||||
apiMux.HandleFunc("/get_random_files", api.HandleGetRandomFiles)
|
||||
apiMux.HandleFunc("/get_random_files_with_tag", api.HandleGetRandomFilesWithTag)
|
||||
apiMux.HandleFunc("/get_file_stream", api.HandleGetFileStream)
|
||||
apiMux.HandleFunc("/get_ffmpeg_config_list", api.HandleGetFfmpegConfigs)
|
||||
apiMux.HandleFunc("/feedback", api.HandleFeedback)
|
||||
apiMux.HandleFunc("/get_file_info", api.HandleGetFileInfo)
|
||||
apiMux.HandleFunc("/get_file_stream_direct", api.HandleGetFileStreamDirect)
|
||||
apiMux.HandleFunc("/prepare_file_stream_direct", api.HandlePrepareFileStreamDirect)
|
||||
// below needs token
|
||||
apiMux.HandleFunc("/delete_file", api.HandleDeleteFile)
|
||||
apiMux.HandleFunc("/update_filename", api.HandleUpdateFilename)
|
||||
apiMux.HandleFunc("/reset_filename", api.HandleResetFilename)
|
||||
apiMux.HandleFunc("/reset_foldername", api.HandleResetFoldername)
|
||||
// feedback
|
||||
apiMux.HandleFunc("/feedback", api.HandleFeedback)
|
||||
apiMux.HandleFunc("/get_feedbacks", api.HandleGetFeedbacks)
|
||||
apiMux.HandleFunc("/delete_feedback", api.HandleDeleteFeedback)
|
||||
// user
|
||||
apiMux.HandleFunc("/login", api.HandleLogin)
|
||||
apiMux.HandleFunc("/register", api.HandleRegister)
|
||||
apiMux.HandleFunc("/logout", api.LoginAsAnonymous)
|
||||
apiMux.HandleFunc("/get_user_info", api.HandleGetUserInfo)
|
||||
apiMux.HandleFunc("/get_users", api.HandleGetUsers)
|
||||
apiMux.HandleFunc("/update_user_active", api.HandleUpdateUserActive)
|
||||
apiMux.HandleFunc("/update_username", api.HandleUpdateUsername)
|
||||
apiMux.HandleFunc("/update_user_password", api.HandleUpdateUserPassword)
|
||||
// tag
|
||||
apiMux.HandleFunc("/get_tags", api.HandleGetTags)
|
||||
apiMux.HandleFunc("/get_tag_info", api.HandleGetTagInfo)
|
||||
apiMux.HandleFunc("/insert_tag", api.HandleInsertTag)
|
||||
apiMux.HandleFunc("/update_tag", api.HandleUpdateTag)
|
||||
apiMux.HandleFunc("/put_tag_on_file", api.HandlePutTagOnFile)
|
||||
apiMux.HandleFunc("/get_tags_on_file", api.HandleGetTagsOnFile)
|
||||
apiMux.HandleFunc("/delete_tag_on_file", api.HandleDeleteTagOnFile)
|
||||
apiMux.HandleFunc("/delete_tag", api.HandleDeleteTag)
|
||||
// folder
|
||||
apiMux.HandleFunc("/update_foldername", api.HandleUpdateFoldername)
|
||||
// review
|
||||
apiMux.HandleFunc("/insert_review", api.HandleInsertReview)
|
||||
apiMux.HandleFunc("/get_reviews_on_file", api.HandleGetReviewsOnFile)
|
||||
apiMux.HandleFunc("/get_review", api.HandleGetReview)
|
||||
apiMux.HandleFunc("/update_review", api.HandleUpdateReview)
|
||||
apiMux.HandleFunc("/delete_review", api.HandleDeleteReview)
|
||||
apiMux.HandleFunc("/get_reviews_by_user", api.HandleGetReviewsByUser)
|
||||
// below needs admin
|
||||
apiMux.HandleFunc("/walk", api.HandleWalk)
|
||||
apiMux.HandleFunc("/reset", api.HandleReset)
|
||||
apiMux.HandleFunc("/add_ffmpeg_config", api.HandleAddFfmpegConfig)
|
||||
|
||||
mux.Handle("/api/v1/", http.StripPrefix("/api/v1", apiMux))
|
||||
mux.Handle("/", http.StripPrefix("/", http.FileServer(http.Dir("web/build"))))
|
||||
|
||||
api.token = apiConfig.Token
|
||||
|
||||
return api, nil
|
||||
}
|
||||
|
||||
@@ -2,32 +2,26 @@ package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type WalkRequest struct {
|
||||
Token string `json:"token"`
|
||||
Root string `json:"root"`
|
||||
Pattern []string `json:"pattern"`
|
||||
}
|
||||
|
||||
type ResetRequest struct {
|
||||
Token string `json:"token"`
|
||||
TagIDs []int64 `json:"tag_ids"`
|
||||
}
|
||||
|
||||
func (api *API) HandleReset(w http.ResponseWriter, r *http.Request) {
|
||||
resetRequest := &ResetRequest{}
|
||||
err := json.NewDecoder(r.Body).Decode(resetRequest)
|
||||
var err error
|
||||
// check admin
|
||||
err = api.CheckAdmin(w, r)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// check token
|
||||
err = api.CheckToken(w, r, resetRequest.Token)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
log.Println("[api] Reset database")
|
||||
|
||||
// reset
|
||||
err = api.Db.ResetFiles()
|
||||
@@ -52,9 +46,10 @@ func (api *API) HandleWalk(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// check token match
|
||||
err = api.CheckToken(w, r, walkRequest.Token)
|
||||
// check admin
|
||||
err = api.CheckAdmin(w, r)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -70,8 +65,17 @@ func (api *API) HandleWalk(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// get userID
|
||||
userID, err := api.GetUserID(w, r)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("[api] Walk", walkRequest.Root, walkRequest.Pattern, walkRequest.TagIDs)
|
||||
|
||||
// walk
|
||||
err = api.Db.Walk(walkRequest.Root, walkRequest.Pattern)
|
||||
err = api.Db.Walk(walkRequest.Root, walkRequest.Pattern, walkRequest.TagIDs, userID)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
|
||||
@@ -2,10 +2,24 @@ package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotLoggedIn = errors.New("not logged in")
|
||||
ErrNotAdmin = errors.New("not admin")
|
||||
ErrEmpty = errors.New("Empty field detected, please fill in all fields")
|
||||
ErrAnonymous = errors.New("Anonymous user detected, please login")
|
||||
ErrNotActive = errors.New("User is not active")
|
||||
ErrWrongPassword = errors.New("Wrong password")
|
||||
)
|
||||
|
||||
type Error struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (api *API) HandleError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
api.HandleErrorString(w, r, err.Error())
|
||||
}
|
||||
@@ -20,8 +34,8 @@ func (api *API) HandleErrorString(w http.ResponseWriter, r *http.Request, errorS
|
||||
|
||||
func (api *API) HandleErrorStringCode(w http.ResponseWriter, r *http.Request, errorString string, code int) {
|
||||
log.Println("[api] [Error]", code, errorString)
|
||||
errStatus := &Status{
|
||||
Status: errorString,
|
||||
errStatus := &Error{
|
||||
Error: errorString,
|
||||
}
|
||||
w.WriteHeader(code)
|
||||
json.NewEncoder(w).Encode(errStatus)
|
||||
|
||||
@@ -4,12 +4,13 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"msw-open-music/pkg/database"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type FeedbackRequest struct {
|
||||
Feedback string `json:"feedback"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func (api *API) HandleFeedback(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -21,12 +22,12 @@ func (api *API) HandleFeedback(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// check empty feedback
|
||||
if feedbackRequest.Feedback == "" {
|
||||
if feedbackRequest.Content == "" {
|
||||
api.HandleErrorString(w, r, `"feedback" can't be empty`)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("[api] Feedback", feedbackRequest.Feedback)
|
||||
log.Println("[api] Feedback", feedbackRequest.Content)
|
||||
|
||||
headerBuff := &bytes.Buffer{}
|
||||
err = r.Header.Write(headerBuff)
|
||||
@@ -36,10 +37,73 @@ func (api *API) HandleFeedback(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
header := headerBuff.String()
|
||||
|
||||
err = api.Db.InsertFeedback(time.Now().Unix(), feedbackRequest.Feedback, header)
|
||||
userID, err := api.GetUserID(w, r)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = api.Db.InsertFeedback(time.Now().Unix(), feedbackRequest.Content, userID, header)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
api.HandleOK(w, r)
|
||||
}
|
||||
|
||||
type GetFeedbacksResponse struct {
|
||||
Feedbacks []*database.Feedback `json:"feedbacks"`
|
||||
}
|
||||
|
||||
func (api *API) HandleGetFeedbacks(w http.ResponseWriter, r *http.Request) {
|
||||
// check if admin
|
||||
err := api.CheckAdmin(w, r)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
feedbacks, err := api.Db.GetFeedbacks()
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := &GetFeedbacksResponse{
|
||||
Feedbacks: feedbacks,
|
||||
}
|
||||
|
||||
err = json.NewEncoder(w).Encode(resp)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type DeleteFeedbackRequest struct {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (api *API) HandleDeleteFeedback(w http.ResponseWriter, r *http.Request) {
|
||||
// check if admin
|
||||
err := api.CheckAdmin(w, r)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := &DeleteFeedbackRequest{}
|
||||
err = json.NewDecoder(r.Body).Decode(req)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = api.Db.DeleteFeedback(req.ID)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.HandleOK(w, r)
|
||||
}
|
||||
|
||||
@@ -3,20 +3,12 @@ package api
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"msw-open-music/pkg/commonconfig"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type FfmpegConfig struct {
|
||||
Name string `json:"name"`
|
||||
Args string `json:"args"`
|
||||
}
|
||||
|
||||
type FfmpegConfigList struct {
|
||||
FfmpegConfigList []FfmpegConfig `json:"ffmpeg_config_list"`
|
||||
}
|
||||
|
||||
func (api *API) GetFfmpegConfig(configName string) (FfmpegConfig, bool) {
|
||||
ffmpegConfig := FfmpegConfig{}
|
||||
func (api *API) GetFfmpegConfig(configName string) (commonconfig.FfmpegConfig, bool) {
|
||||
ffmpegConfig := commonconfig.FfmpegConfig{}
|
||||
for _, f := range api.APIConfig.FfmpegConfigList {
|
||||
if f.Name == configName {
|
||||
ffmpegConfig = f
|
||||
@@ -30,45 +22,8 @@ func (api *API) GetFfmpegConfig(configName string) (FfmpegConfig, bool) {
|
||||
|
||||
func (api *API) HandleGetFfmpegConfigs(w http.ResponseWriter, r *http.Request) {
|
||||
log.Println("[api] Get ffmpeg config list")
|
||||
ffmpegConfigList := &FfmpegConfigList{
|
||||
ffmpegConfigList := &commonconfig.FfmpegConfigList{
|
||||
FfmpegConfigList: api.APIConfig.FfmpegConfigList,
|
||||
}
|
||||
json.NewEncoder(w).Encode(&ffmpegConfigList)
|
||||
}
|
||||
|
||||
type AddFfmpegConfigRequest struct {
|
||||
Token string `json:"token"`
|
||||
Name string `json:"name"`
|
||||
FfmpegConfig FfmpegConfig `json:"ffmpeg_config"`
|
||||
}
|
||||
|
||||
func (api *API) HandleAddFfmpegConfig(w http.ResponseWriter, r *http.Request) {
|
||||
addFfmpegConfigRequest := AddFfmpegConfigRequest{}
|
||||
err := json.NewDecoder(r.Body).Decode(&addFfmpegConfigRequest)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// check token
|
||||
err = api.CheckToken(w, r, addFfmpegConfigRequest.Token)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// check name and args not null
|
||||
if addFfmpegConfigRequest.Name == "" {
|
||||
api.HandleErrorString(w, r, `"ffmpeg_config.name" can't be empty`)
|
||||
return
|
||||
}
|
||||
if addFfmpegConfigRequest.FfmpegConfig.Args == "" {
|
||||
api.HandleErrorString(w, r, `"ffmpeg_config.args" can't be empty`)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("[api] Add ffmpeg config")
|
||||
|
||||
api.APIConfig.FfmpegConfigList = append(api.APIConfig.FfmpegConfigList, addFfmpegConfigRequest.FfmpegConfig)
|
||||
|
||||
api.HandleOK(w, r)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
@@ -30,6 +31,8 @@ func (api *API) HandleGetFileInfo(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("[api] Get file info", getFileRequest.ID)
|
||||
|
||||
file, err := api.Db.GetFile(getFileRequest.ID)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
@@ -111,6 +114,12 @@ func (api *API) HandleGetFileDirect(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// set header for filename
|
||||
filename := file.Filename
|
||||
// encode filename to URL
|
||||
filename = url.PathEscape(filename)
|
||||
w.Header().Set("Content-Disposition", "inline; filename*=UTF-8''"+filename)
|
||||
|
||||
log.Println("[api] Get direct raw file", path)
|
||||
|
||||
http.ServeFile(w, r, path)
|
||||
|
||||
@@ -23,3 +23,29 @@ func (api *API) HandleGetRandomFiles(w http.ResponseWriter, r *http.Request) {
|
||||
log.Println("[api] Get random files")
|
||||
json.NewEncoder(w).Encode(getRandomFilesResponse)
|
||||
}
|
||||
|
||||
type GetRandomFilesWithTagRequest struct {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (api *API) HandleGetRandomFilesWithTag(w http.ResponseWriter, r *http.Request) {
|
||||
req := &GetRandomFilesWithTagRequest{}
|
||||
err := json.NewDecoder(r.Body).Decode(req)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
files, err := api.Db.GetRandomFilesWithTag(req.ID, 10)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
getRandomFilesResponse := &GetRandomFilesResponse{
|
||||
Files: &files,
|
||||
}
|
||||
|
||||
log.Println("[api] Get random files with tag", req.ID)
|
||||
json.NewEncoder(w).Encode(getRandomFilesResponse)
|
||||
}
|
||||
|
||||
98
pkg/api/handle_manage_file.go
Normal file
98
pkg/api/handle_manage_file.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type DeleteFileRequest struct {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (api *API) HandleDeleteFile(w http.ResponseWriter, r *http.Request) {
|
||||
// check admin
|
||||
err := api.CheckAdmin(w, r)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := &DeleteFileRequest{}
|
||||
err = json.NewDecoder(r.Body).Decode(req)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("[api] delete file", req.ID)
|
||||
|
||||
err = api.Db.DeleteFile(req.ID)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.HandleOK(w, r)
|
||||
}
|
||||
|
||||
type UpdateFilenameRequest struct {
|
||||
ID int64 `json:"id"`
|
||||
Filename string `json:"filename"`
|
||||
}
|
||||
|
||||
func (api *API) HandleUpdateFilename(w http.ResponseWriter, r *http.Request) {
|
||||
// check admin
|
||||
err := api.CheckAdmin(w, r)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := &UpdateFilenameRequest{}
|
||||
err = json.NewDecoder(r.Body).Decode(req)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("[api] update filename", req.ID, req.Filename)
|
||||
|
||||
err = api.Db.UpdateFilename(req.ID, req.Filename)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.HandleOK(w, r)
|
||||
}
|
||||
|
||||
type ResetFilenameRequest struct {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (api *API) HandleResetFilename(w http.ResponseWriter, r *http.Request) {
|
||||
// check admin
|
||||
err := api.CheckAdmin(w, r)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := &ResetFilenameRequest{}
|
||||
err = json.NewDecoder(r.Body).Decode(req)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("[api] reset filename", req.ID)
|
||||
|
||||
err = api.Db.ResetFilename(req.ID)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.HandleOK(w, r)
|
||||
}
|
||||
68
pkg/api/handle_manage_folder.go
Normal file
68
pkg/api/handle_manage_folder.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type ResetFoldernameRequest struct {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (api *API) HandleResetFoldername(w http.ResponseWriter, r *http.Request) {
|
||||
// check admin
|
||||
err := api.CheckAdmin(w, r)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := &ResetFoldernameRequest{}
|
||||
err = json.NewDecoder(r.Body).Decode(req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("[api] Reset foldername folderID", req.ID)
|
||||
|
||||
err = api.Db.ResetFoldername(req.ID)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.HandleOK(w, r)
|
||||
}
|
||||
|
||||
type UpdateFoldernameRequest struct {
|
||||
ID int64 `json:"id"`
|
||||
Foldername string `json:"foldername"`
|
||||
}
|
||||
|
||||
func (api *API) HandleUpdateFoldername(w http.ResponseWriter, r *http.Request) {
|
||||
req := &UpdateFoldernameRequest{}
|
||||
err := json.NewDecoder(r.Body).Decode(req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// check is admin
|
||||
err = api.CheckAdmin(w, r)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("[api] Update foldername folderID", req.ID, req.Foldername)
|
||||
|
||||
err = api.Db.UpdateFoldername(req.ID, req.Foldername)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
api.HandleOK(w, r)
|
||||
}
|
||||
220
pkg/api/handle_review.go
Normal file
220
pkg/api/handle_review.go
Normal file
@@ -0,0 +1,220 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"msw-open-music/pkg/database"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// review.FileId, review.Content
|
||||
func (api *API) HandleInsertReview(w http.ResponseWriter, r *http.Request) {
|
||||
review := &database.Review{}
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(review)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
review.UserId, err = api.GetUserID(w, r)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("[api] Insert review by", review.UserId, review.Content)
|
||||
|
||||
review.CreatedAt = time.Now().Unix()
|
||||
|
||||
err = api.Db.InsertReview(review)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.HandleOK(w, r)
|
||||
}
|
||||
|
||||
type GetReviewsOnFileRequest struct {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
type GetReviewsOnFileResponse struct {
|
||||
Reviews []*database.Review `json:"reviews"`
|
||||
}
|
||||
|
||||
func (api *API) HandleGetReviewsOnFile(w http.ResponseWriter, r *http.Request) {
|
||||
req := &GetReviewsOnFileRequest{}
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(req)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
reviews, err := api.Db.GetReviewsOnFile(req.ID)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("[api] Get reviews on fileID", req.ID)
|
||||
|
||||
resp := &GetReviewsOnFileResponse{
|
||||
Reviews: reviews,
|
||||
}
|
||||
|
||||
err = json.NewEncoder(w).Encode(resp)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type GetReviewRequest struct {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
type GetReviewResponse struct {
|
||||
Review *database.Review `json:"review"`
|
||||
}
|
||||
|
||||
func (api *API) HandleGetReview(w http.ResponseWriter, r *http.Request) {
|
||||
req := &GetReviewRequest{}
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(req)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
review, err := api.Db.GetReview(req.ID)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("[api] Get review ID", req.ID)
|
||||
|
||||
ret := &GetReviewResponse{
|
||||
Review: review,
|
||||
}
|
||||
|
||||
err = json.NewEncoder(w).Encode(ret)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (api *API) CheckUserCanModifyReview(w http.ResponseWriter, r *http.Request, reviewID int64) error {
|
||||
review, err := api.Db.GetReview(reviewID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = api.CheckNotAnonymous(w, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = api.CheckAdmin(w, r)
|
||||
if err != nil {
|
||||
userID, err := api.GetUserID(w, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if review.UserId != userID {
|
||||
return errors.New("you are not allowed to modify this review")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *API) HandleUpdateReview(w http.ResponseWriter, r *http.Request) {
|
||||
req := &database.Review{}
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(req)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = api.CheckUserCanModifyReview(w, r, req.ID)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("[api] Update review", req.ID, req.Content)
|
||||
|
||||
req.UpdatedAt = time.Now().Unix()
|
||||
|
||||
err = api.Db.UpdateReview(req)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.HandleOK(w, r)
|
||||
}
|
||||
|
||||
type DeleteReviewRequest struct {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (api *API) HandleDeleteReview(w http.ResponseWriter, r *http.Request) {
|
||||
req := &DeleteReviewRequest{}
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(req)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = api.CheckUserCanModifyReview(w, r, req.ID)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("[api] Delete review ID", req.ID)
|
||||
|
||||
err = api.Db.DeleteReview(req.ID)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.HandleOK(w, r)
|
||||
}
|
||||
|
||||
type GetReviewsByUserRequest struct {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (api *API) HandleGetReviewsByUser(w http.ResponseWriter, r *http.Request) {
|
||||
req := &GetReviewsByUserRequest{}
|
||||
err := json.NewDecoder(r.Body).Decode(req)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
reviews, err := api.Db.GetReviewsByUser(req.ID)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = json.NewEncoder(w).Encode(reviews)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,9 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"msw-open-music/pkg/database"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
@@ -65,13 +67,21 @@ func (api *API) HandleGetFileStream(w http.ResponseWriter, r *http.Request) {
|
||||
api.HandleErrorStringCode(w, r, `ffmpeg config not found`, 404)
|
||||
return
|
||||
}
|
||||
|
||||
// set headers for filename
|
||||
filename := file.Filename + "." + ffmpegConfig.Name + "." + ffmpegConfig.Format
|
||||
filename = url.PathEscape(filename)
|
||||
// replace invalid characters
|
||||
w.Header().Set("Content-Disposition", "inline; filename*=UTF-8''"+filename)
|
||||
|
||||
args := strings.Split(ffmpegConfig.Args, " ")
|
||||
startArgs := []string{"-threads", strconv.FormatInt(api.APIConfig.FfmpegThreads, 10), "-i", path}
|
||||
endArgs := []string{"-vn", "-f", "ogg", "-"}
|
||||
endArgs := []string{"-f", ffmpegConfig.Format, "-"}
|
||||
ffmpegArgs := append(startArgs, args...)
|
||||
ffmpegArgs = append(ffmpegArgs, endArgs...)
|
||||
cmd := exec.Command("ffmpeg", ffmpegArgs...)
|
||||
cmd.Stdout = w
|
||||
// cmd.Stderr = os.Stderr
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
@@ -85,7 +95,7 @@ type PrepareFileStreamDirectRequest struct {
|
||||
}
|
||||
|
||||
type PrepareFileStreamDirectResponse struct {
|
||||
Filesize int64 `json:"filesize"`
|
||||
File *database.File `json:"file"`
|
||||
}
|
||||
|
||||
// /prepare_file_stream_direct?id=1&config=ffmpeg_config_name
|
||||
@@ -126,27 +136,17 @@ func (api *API) HandlePrepareFileStreamDirect(w http.ResponseWriter, r *http.Req
|
||||
api.HandleErrorStringCode(w, r, `ffmpeg config not found`, 404)
|
||||
return
|
||||
}
|
||||
objPath := api.Tmpfs.GetObjFilePath(prepareFileStreamDirectRequst.ID, prepareFileStreamDirectRequst.ConfigName)
|
||||
objPath := api.Tmpfs.GetObjFilePath(prepareFileStreamDirectRequst.ID, ffmpegConfig)
|
||||
|
||||
// check obj file exists
|
||||
exists := api.Tmpfs.Exits(objPath)
|
||||
if exists {
|
||||
fileInfo, err := os.Stat(objPath)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
prepareFileStreamDirectResponse := &PrepareFileStreamDirectResponse{
|
||||
Filesize: fileInfo.Size(),
|
||||
}
|
||||
json.NewEncoder(w).Encode(prepareFileStreamDirectResponse)
|
||||
return
|
||||
}
|
||||
if !exists {
|
||||
// lock the object
|
||||
api.Tmpfs.Lock(objPath)
|
||||
|
||||
api.Tmpfs.Record(objPath)
|
||||
args := strings.Split(ffmpegConfig.Args, " ")
|
||||
startArgs := []string{"-threads", strconv.FormatInt(api.APIConfig.FfmpegThreads, 10), "-i", srcPath}
|
||||
endArgs := []string{"-vn", "-y", objPath}
|
||||
endArgs := []string{"-y", objPath}
|
||||
ffmpegArgs := append(startArgs, args...)
|
||||
ffmpegArgs = append(ffmpegArgs, endArgs...)
|
||||
cmd := exec.Command("ffmpeg", ffmpegArgs...)
|
||||
@@ -156,13 +156,21 @@ func (api *API) HandlePrepareFileStreamDirect(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
|
||||
api.Tmpfs.Record(objPath)
|
||||
api.Tmpfs.Unlock(objPath)
|
||||
|
||||
}
|
||||
|
||||
fileInfo, err := os.Stat(objPath)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
file.Filesize = fileInfo.Size()
|
||||
|
||||
prepareFileStreamDirectResponse := &PrepareFileStreamDirectResponse{
|
||||
Filesize: fileInfo.Size(),
|
||||
File: file,
|
||||
}
|
||||
json.NewEncoder(w).Encode(prepareFileStreamDirectResponse)
|
||||
}
|
||||
@@ -180,11 +188,23 @@ func (api *API) HandleGetFileStreamDirect(w http.ResponseWriter, r *http.Request
|
||||
configs := q["config"]
|
||||
configName := configs[0]
|
||||
|
||||
path := api.Tmpfs.GetObjFilePath(int64(id), configName)
|
||||
ffmpegConfig, ok := api.GetFfmpegConfig(configName)
|
||||
if !ok {
|
||||
api.HandleErrorStringCode(w, r, `ffmpeg config not found`, 404)
|
||||
return
|
||||
}
|
||||
|
||||
path := api.Tmpfs.GetObjFilePath(int64(id), ffmpegConfig)
|
||||
if api.Tmpfs.Exits(path) {
|
||||
api.Tmpfs.Record(path)
|
||||
}
|
||||
|
||||
// set headers for filename
|
||||
filename := ids[0] + "." + ffmpegConfig.Name + "." + ffmpegConfig.Format
|
||||
filename = url.PathEscape(filename)
|
||||
// replace invalid characters
|
||||
w.Header().Set("Content-Disposition", "inline; filename*=UTF-8''"+filename)
|
||||
|
||||
log.Println("[api] Get direct cached file", path)
|
||||
|
||||
http.ServeFile(w, r, path)
|
||||
|
||||
161
pkg/api/handle_tag.go
Normal file
161
pkg/api/handle_tag.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"msw-open-music/pkg/database"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type getTagsResponse struct {
|
||||
Tags []*database.Tag `json:"tags"`
|
||||
}
|
||||
|
||||
func (api *API) HandleGetTags(w http.ResponseWriter, r *http.Request) {
|
||||
tags, err := api.Db.GetTags()
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("[api] Successfully got tags")
|
||||
|
||||
resp := &getTagsResponse{Tags: tags}
|
||||
|
||||
err = json.NewEncoder(w).Encode(resp)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type InsertTagResponse struct {
|
||||
Tag *database.Tag `json:"tag"`
|
||||
}
|
||||
|
||||
func (api *API) HandleInsertTag(w http.ResponseWriter, r *http.Request) {
|
||||
// check if user is admin
|
||||
err := api.CheckAdmin(w, r)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := &database.Tag{}
|
||||
err = json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
req.CreatedByUserId, err = api.GetUserID(w, r)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
tagID, err := api.Db.InsertTag(req)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
tag, err := api.Db.GetTag(tagID)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := &InsertTagResponse{Tag: tag}
|
||||
|
||||
err = json.NewEncoder(w).Encode(resp)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type GetTagInfoRequest struct {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
type GetTagInfoResponse struct {
|
||||
Tag *database.Tag `json:"tag"`
|
||||
}
|
||||
|
||||
func (api *API) HandleGetTagInfo(w http.ResponseWriter, r *http.Request) {
|
||||
var req GetTagInfoRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
tag, err := api.Db.GetTag(req.ID)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := &GetTagInfoResponse{Tag: tag}
|
||||
|
||||
err = json.NewEncoder(w).Encode(resp)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (api *API) HandleUpdateTag(w http.ResponseWriter, r *http.Request) {
|
||||
// check if user is admin
|
||||
err := api.CheckAdmin(w, r)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := &database.Tag{}
|
||||
err = json.NewDecoder(r.Body).Decode(req)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = api.Db.UpdateTag(req)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.HandleOK(w, r)
|
||||
}
|
||||
|
||||
type DeleteTagRequest struct {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (api *API) HandleDeleteTag(w http.ResponseWriter, r *http.Request) {
|
||||
// check if user is admin
|
||||
err := api.CheckAdmin(w, r)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := &DeleteTagRequest{}
|
||||
err = json.NewDecoder(r.Body).Decode(req)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = api.Db.DeleteTag(req.ID)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("[api] Successfully deleted tag and its references", req.ID)
|
||||
|
||||
api.HandleOK(w, r)
|
||||
}
|
||||
119
pkg/api/handle_tag_and_file.go
Normal file
119
pkg/api/handle_tag_and_file.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"msw-open-music/pkg/database"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type PutTagOnFileRequest struct {
|
||||
TagID int64 `json:"tag_id"`
|
||||
FileID int64 `json:"file_id"`
|
||||
}
|
||||
|
||||
func (api *API) HandlePutTagOnFile(w http.ResponseWriter, r *http.Request) {
|
||||
// check if the user is admin
|
||||
err := api.CheckAdmin(w, r)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := &PutTagOnFileRequest{}
|
||||
err = json.NewDecoder(r.Body).Decode(req)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := api.GetUserID(w, r)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// check empty
|
||||
if req.TagID == 0 || req.FileID == 0 {
|
||||
api.HandleError(w, r, ErrEmpty)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("[api] Put tag on file request:", req, "userID:", userID)
|
||||
|
||||
api.Db.PutTagOnFile(req.TagID, req.FileID, userID)
|
||||
|
||||
api.HandleOK(w, r)
|
||||
}
|
||||
|
||||
type GetTagsOnFileRequest struct {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
type GetTagsOnFileResponse struct {
|
||||
Tags []*database.Tag `json:"tags"`
|
||||
}
|
||||
|
||||
func (api *API) HandleGetTagsOnFile(w http.ResponseWriter, r *http.Request) {
|
||||
req := &GetTagsOnFileRequest{}
|
||||
err := json.NewDecoder(r.Body).Decode(req)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("[api] Get tags on file request:", req)
|
||||
|
||||
tags, err := api.Db.GetTagsOnFile(req.ID)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := &GetTagsOnFileResponse{
|
||||
Tags: tags,
|
||||
}
|
||||
|
||||
err = json.NewEncoder(w).Encode(resp)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type DeleteTagOnFileRequest struct {
|
||||
TagID int64 `json:"tag_id"`
|
||||
FileID int64 `json:"file_id"`
|
||||
}
|
||||
|
||||
func (api *API) HandleDeleteTagOnFile(w http.ResponseWriter, r *http.Request) {
|
||||
// check if the user is admin
|
||||
err := api.CheckAdmin(w, r)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := &DeleteTagOnFileRequest{}
|
||||
err = json.NewDecoder(r.Body).Decode(req)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// check empty
|
||||
if req.TagID == 0 || req.FileID == 0 {
|
||||
api.HandleError(w, r, ErrEmpty)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("[api] Delete tag on file request:", req)
|
||||
|
||||
err = api.Db.DeleteTagOnFile(req.TagID, req.FileID)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.HandleOK(w, r)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (api *API) CheckToken(w http.ResponseWriter, r *http.Request, token string) error {
|
||||
if token != api.token {
|
||||
err := errors.New("token not matched")
|
||||
log.Println("[api] [Warning] Token not matched", token)
|
||||
api.HandleErrorCode(w, r, err, 403)
|
||||
return err
|
||||
}
|
||||
log.Println("[api] Token passed")
|
||||
return nil
|
||||
}
|
||||
376
pkg/api/handle_user.go
Normal file
376
pkg/api/handle_user.go
Normal file
@@ -0,0 +1,376 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"msw-open-music/pkg/database"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type LoginResponse struct {
|
||||
User *database.User `json:"user"`
|
||||
}
|
||||
|
||||
func (api *API) LoginAsAnonymous(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := api.Db.LoginAsAnonymous()
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
session, _ := api.store.Get(r, api.defaultSessionName)
|
||||
|
||||
// save session
|
||||
session.Values["userId"] = user.ID
|
||||
err = session.Save(r, w)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := &LoginResponse{
|
||||
User: user,
|
||||
}
|
||||
|
||||
err = json.NewEncoder(w).Encode(resp)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (api *API) HandleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
var user *database.User
|
||||
var err error
|
||||
session, _ := api.store.Get(r, api.defaultSessionName)
|
||||
|
||||
// Get method will login current or anonymous user
|
||||
if r.Method == "GET" {
|
||||
|
||||
// if user already logged in
|
||||
if userId, ok := session.Values["userId"]; ok {
|
||||
user, err = api.Db.GetUserById(userId.(int64))
|
||||
if err != nil {
|
||||
if err != sql.ErrNoRows {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
log.Println("[api] Warning: User not found")
|
||||
// login as anonymous user
|
||||
api.LoginAsAnonymous(w, r)
|
||||
return
|
||||
}
|
||||
log.Println("[api] User already logged in:", user)
|
||||
|
||||
} else {
|
||||
// login as anonymous user
|
||||
log.Println("[api] Login as anonymous user")
|
||||
api.LoginAsAnonymous(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
var request LoginRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&request)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("Login as user", request.Username)
|
||||
|
||||
user, err = api.Db.Login(request.Username, request.Password)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// if user is not active
|
||||
if !user.Active {
|
||||
api.HandleError(w, r, ErrNotActive)
|
||||
return
|
||||
}
|
||||
|
||||
// save session
|
||||
session.Values["userId"] = user.ID
|
||||
err = session.Save(r, w)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := &LoginResponse{
|
||||
User: user,
|
||||
}
|
||||
err = json.NewEncoder(w).Encode(resp)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type RegisterRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Role int64 `json:"role"`
|
||||
}
|
||||
|
||||
func (api *API) HandleRegister(w http.ResponseWriter, r *http.Request) {
|
||||
var request RegisterRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&request)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("[api] Register user", request.Username)
|
||||
|
||||
err = api.Db.Register(request.Username, request.Password, request.Role)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.HandleOK(w, r)
|
||||
}
|
||||
|
||||
func (api *API) CheckAdmin(w http.ResponseWriter, r *http.Request) error {
|
||||
session, _ := api.store.Get(r, api.defaultSessionName)
|
||||
userId, ok := session.Values["userId"]
|
||||
if !ok {
|
||||
return ErrNotLoggedIn
|
||||
}
|
||||
|
||||
user, err := api.Db.GetUserById(userId.(int64))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if user.Role != database.RoleAdmin {
|
||||
return ErrNotAdmin
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *API) CheckNotAnonymous(w http.ResponseWriter, r *http.Request) error {
|
||||
session, _ := api.store.Get(r, api.defaultSessionName)
|
||||
userId, ok := session.Values["userId"]
|
||||
if !ok {
|
||||
return ErrNotLoggedIn
|
||||
}
|
||||
|
||||
user, err := api.Db.GetUserById(userId.(int64))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if user.Role == database.RoleAnonymous {
|
||||
return ErrAnonymous
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *API) GetUserID(w http.ResponseWriter, r *http.Request) (int64, error) {
|
||||
session, _ := api.store.Get(r, api.defaultSessionName)
|
||||
userId, ok := session.Values["userId"]
|
||||
if !ok {
|
||||
return 0, ErrNotLoggedIn
|
||||
}
|
||||
|
||||
return userId.(int64), nil
|
||||
}
|
||||
|
||||
type GetUsersResponse struct {
|
||||
Users []*database.User `json:"users"`
|
||||
}
|
||||
|
||||
func (api *API) HandleGetUsers(w http.ResponseWriter, r *http.Request) {
|
||||
users, err := api.Db.GetUsers()
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
ret := &GetUsersResponse{
|
||||
Users: users,
|
||||
}
|
||||
|
||||
err = json.NewEncoder(w).Encode(ret)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type UpdateUserActiveRequest struct {
|
||||
ID int64 `json:"id"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
func (api *API) HandleUpdateUserActive(w http.ResponseWriter, r *http.Request) {
|
||||
err := api.CheckAdmin(w, r)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := &UpdateUserActiveRequest{}
|
||||
err = json.NewDecoder(r.Body).Decode(req)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = api.Db.UpdateUserActive(req.ID, req.Active)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
api.HandleOK(w, r)
|
||||
}
|
||||
|
||||
type UpdateUsernameRequest struct {
|
||||
ID int64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
func (api *API) HandleUpdateUsername(w http.ResponseWriter, r *http.Request) {
|
||||
// reject anonymous user
|
||||
err := api.CheckNotAnonymous(w, r)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := &UpdateUsernameRequest{}
|
||||
|
||||
err = json.NewDecoder(r.Body).Decode(req)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := api.Db.GetUserById(req.ID)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := api.GetUserID(w, r)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if user.ID != userID && user.Role != database.RoleAdmin {
|
||||
api.HandleError(w, r, ErrNotAdmin)
|
||||
return
|
||||
}
|
||||
|
||||
err = api.Db.UpdateUsername(req.ID, req.Username)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.HandleOK(w, r)
|
||||
}
|
||||
|
||||
type GetUserInfoRequest struct {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
type GetUserInfoResponse struct {
|
||||
User *database.User `json:"user"`
|
||||
}
|
||||
|
||||
func (api *API) HandleGetUserInfo(w http.ResponseWriter, r *http.Request) {
|
||||
req := &GetUserInfoRequest{}
|
||||
err := json.NewDecoder(r.Body).Decode(req)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := api.Db.GetUserById(req.ID)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
ret := &GetUserInfoResponse{
|
||||
User: user,
|
||||
}
|
||||
|
||||
err = json.NewEncoder(w).Encode(ret)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type UpdateUserPasswordRequest struct {
|
||||
ID int64 `json:"id"`
|
||||
OldPassword string `json:"old_password"`
|
||||
NewPassword string `json:"new_password"`
|
||||
}
|
||||
|
||||
func (api *API) HandleUpdateUserPassword(w http.ResponseWriter, r *http.Request) {
|
||||
// reject anonymous user
|
||||
err := api.CheckNotAnonymous(w, r)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := &UpdateUserPasswordRequest{}
|
||||
err = json.NewDecoder(r.Body).Decode(req)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := api.Db.GetUserById(req.ID)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := api.GetUserID(w, r)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
currentUser, err := api.Db.GetUserById(userID)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if currentUser.Role != database.RoleAdmin {
|
||||
_, err := api.Db.Login(user.Username, req.OldPassword)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, ErrWrongPassword)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = api.Db.UpdateUserPassword(req.ID, req.NewPassword)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.HandleOK(w, r)
|
||||
}
|
||||
43
pkg/commonconfig/config.go
Normal file
43
pkg/commonconfig/config.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package commonconfig
|
||||
|
||||
type Config struct {
|
||||
APIConfig APIConfig `json:"api"`
|
||||
TmpfsConfig TmpfsConfig `json:"tmpfs"`
|
||||
}
|
||||
|
||||
type APIConfig struct {
|
||||
DatabaseName string `json:"database_name"`
|
||||
SingleThread bool `json:"single_thread,default=true"`
|
||||
Addr string `json:"addr"`
|
||||
FfmpegThreads int64 `json:"ffmpeg_threads"`
|
||||
FfmpegConfigList []FfmpegConfig `json:"ffmpeg_config_list"`
|
||||
SECRET string `json:"secret"`
|
||||
}
|
||||
|
||||
type FfmpegConfigList struct {
|
||||
FfmpegConfigList []FfmpegConfig `json:"ffmpeg_config_list"`
|
||||
}
|
||||
|
||||
type FfmpegConfig struct {
|
||||
Name string `json:"name"`
|
||||
Args string `json:"args"`
|
||||
Format string `json:"format"`
|
||||
}
|
||||
|
||||
type TmpfsConfig struct {
|
||||
FileLifeTime int64 `json:"file_life_time"`
|
||||
CleanerInternal int64 `json:"cleaner_internal"`
|
||||
Root string `json:"root"`
|
||||
}
|
||||
|
||||
// Constructors for Config
|
||||
|
||||
func NewAPIConfig() APIConfig {
|
||||
apiConfig := APIConfig{}
|
||||
return apiConfig
|
||||
}
|
||||
|
||||
func NewTmpfsConfig() *TmpfsConfig {
|
||||
config := &TmpfsConfig{}
|
||||
return config
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"sync"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
@@ -9,9 +10,34 @@ import (
|
||||
type Database struct {
|
||||
sqlConn *sql.DB
|
||||
stmt *Stmt
|
||||
singleThreadLock SingleThreadLock
|
||||
}
|
||||
|
||||
func NewDatabase(dbName string) (*Database, error) {
|
||||
func NewSingleThreadLock(enabled bool) SingleThreadLock {
|
||||
return SingleThreadLock{
|
||||
lock: sync.Mutex{},
|
||||
enabled: enabled,
|
||||
}
|
||||
}
|
||||
|
||||
type SingleThreadLock struct {
|
||||
lock sync.Mutex
|
||||
enabled bool
|
||||
}
|
||||
|
||||
func (stl *SingleThreadLock) Lock() {
|
||||
if stl.enabled {
|
||||
stl.lock.Lock()
|
||||
}
|
||||
}
|
||||
|
||||
func (stl *SingleThreadLock) Unlock() {
|
||||
if stl.enabled {
|
||||
stl.lock.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func NewDatabase(dbName string, singleThread bool) (*Database, error) {
|
||||
var err error
|
||||
|
||||
// open database
|
||||
@@ -30,6 +56,7 @@ func NewDatabase(dbName string) (*Database, error) {
|
||||
database := &Database{
|
||||
sqlConn: sqlConn,
|
||||
stmt: stmt,
|
||||
singleThreadLock: NewSingleThreadLock(singleThread),
|
||||
}
|
||||
|
||||
return database, nil
|
||||
|
||||
10
pkg/database/error.go
Normal file
10
pkg/database/error.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("object not found")
|
||||
ErrTagNotFound = errors.New("tag not found")
|
||||
)
|
||||
@@ -7,15 +7,10 @@ import (
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func (database *Database) InsertFeedback(time int64, feedback string, header string) error {
|
||||
_, err := database.stmt.insertFeedback.Exec(time, feedback, header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (database *Database) GetRandomFiles(limit int64) ([]File, error) {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
rows, err := database.stmt.getRandomFiles.Query(limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -35,7 +30,33 @@ func (database *Database) GetRandomFiles(limit int64) ([]File, error) {
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (database *Database) GetRandomFilesWithTag(tagID, limit int64) ([]File, error) {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
rows, err := database.stmt.getRandomFilesWithTag.Query(tagID, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
files := make([]File, 0)
|
||||
for rows.Next() {
|
||||
file := File{
|
||||
Db: database,
|
||||
}
|
||||
err = rows.Scan(&file.ID, &file.Folder_id, &file.Filename, &file.Foldername, &file.Filesize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (database *Database) GetFilesInFolder(folder_id int64, limit int64, offset int64) ([]File, error) {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
rows, err := database.stmt.getFilesInFolder.Query(folder_id, limit, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -57,6 +78,9 @@ func (database *Database) GetFilesInFolder(folder_id int64, limit int64, offset
|
||||
}
|
||||
|
||||
func (database *Database) SearchFolders(foldername string, limit int64, offset int64) ([]Folder, error) {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
rows, err := database.stmt.searchFolders.Query("%"+foldername+"%", limit, offset)
|
||||
if err != nil {
|
||||
return nil, errors.New("Error searching folders at query " + err.Error())
|
||||
@@ -80,7 +104,11 @@ func (database *Database) GetFile(id int64) (*File, error) {
|
||||
file := &File{
|
||||
Db: database,
|
||||
}
|
||||
err := database.stmt.getFile.QueryRow(id).Scan(&file.ID, &file.Folder_id, &file.Filename, &file.Foldername, &file.Filesize)
|
||||
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
err := database.stmt.getFile.QueryRow(id).Scan(&file.ID, &file.Folder_id, &file.Realname, &file.Filename, &file.Foldername, &file.Filesize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -115,12 +143,22 @@ func (database *Database) ResetFolder() error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (database *Database) Walk(root string, pattern []string) error {
|
||||
func (database *Database) Walk(root string, pattern []string, tagIDs []int64, userID int64) error {
|
||||
patternDict := make(map[string]bool)
|
||||
for _, v := range pattern {
|
||||
patternDict[v] = true
|
||||
}
|
||||
log.Println("[db] Walk", root, patternDict)
|
||||
|
||||
tags := make([]*Tag, 0)
|
||||
for _, tagID := range tagIDs {
|
||||
tag, err := database.GetTag(tagID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
|
||||
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -137,10 +175,17 @@ func (database *Database) Walk(root string, pattern []string) error {
|
||||
}
|
||||
|
||||
// insert file, folder will aut created
|
||||
err = database.Insert(path, info.Size())
|
||||
fileID, err := database.Insert(path, info.Size())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, tag := range tags {
|
||||
err = database.PutTagOnFile(tag.ID, fileID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -149,6 +194,10 @@ func (database *Database) GetFolder(folderId int64) (*Folder, error) {
|
||||
folder := &Folder{
|
||||
Db: database,
|
||||
}
|
||||
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
err := database.stmt.getFolder.QueryRow(folderId).Scan(&folder.Folder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -157,6 +206,9 @@ func (database *Database) GetFolder(folderId int64) (*Folder, error) {
|
||||
}
|
||||
|
||||
func (database *Database) SearchFiles(filename string, limit int64, offset int64) ([]File, error) {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
rows, err := database.stmt.searchFiles.Query("%"+filename+"%", limit, offset)
|
||||
if err != nil {
|
||||
return nil, errors.New("Error searching files at query " + err.Error())
|
||||
@@ -181,6 +233,10 @@ func (database *Database) SearchFiles(filename string, limit int64, offset int64
|
||||
|
||||
func (database *Database) FindFolder(folder string) (int64, error) {
|
||||
var id int64
|
||||
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
err := database.stmt.findFolder.QueryRow(folder).Scan(&id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -190,6 +246,10 @@ func (database *Database) FindFolder(folder string) (int64, error) {
|
||||
|
||||
func (database *Database) FindFile(folderId int64, filename string) (int64, error) {
|
||||
var id int64
|
||||
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
err := database.stmt.findFile.QueryRow(folderId, filename).Scan(&id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -198,6 +258,10 @@ func (database *Database) FindFile(folderId int64, filename string) (int64, erro
|
||||
}
|
||||
|
||||
func (database *Database) InsertFolder(folder string) (int64, error) {
|
||||
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
result, err := database.stmt.insertFolder.Exec(folder, filepath.Base(folder))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -209,31 +273,125 @@ func (database *Database) InsertFolder(folder string) (int64, error) {
|
||||
return lastInsertId, nil
|
||||
}
|
||||
|
||||
func (database *Database) InsertFile(folderId int64, filename string, filesize int64) error {
|
||||
_, err := database.stmt.insertFile.Exec(folderId, filename, filesize)
|
||||
func (database *Database) InsertFile(folderId int64, filename string, filesize int64) (int64, error) {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
result, err := database.stmt.insertFile.Exec(folderId, filename, filename, filesize)
|
||||
if err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
return nil
|
||||
lastInsertId, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return lastInsertId, nil
|
||||
}
|
||||
|
||||
func (database *Database) Insert(path string, filesize int64) error {
|
||||
func (database *Database) Insert(path string, filesize int64) (int64, error) {
|
||||
folder, filename := filepath.Split(path)
|
||||
folderId, err := database.FindFolder(folder)
|
||||
if err != nil {
|
||||
folderId, err = database.InsertFolder(folder)
|
||||
if err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
// if file exists, skip it
|
||||
_, err = database.FindFile(folderId, filename)
|
||||
lastInsertId, err := database.FindFile(folderId, filename)
|
||||
if err == nil {
|
||||
return nil
|
||||
return lastInsertId, nil
|
||||
}
|
||||
|
||||
err = database.InsertFile(folderId, filename, filesize)
|
||||
lastInsertId, err = database.InsertFile(folderId, filename, filesize)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return lastInsertId, nil
|
||||
}
|
||||
|
||||
func (database *Database) UpdateFoldername(folderId int64, foldername string) error {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
_, err := database.stmt.updateFoldername.Exec(foldername, folderId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (database *Database) DeleteFile(fileId int64) error {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
// begin transaction
|
||||
tx, err := database.sqlConn.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// delete file
|
||||
_, err = tx.Stmt(database.stmt.deleteFile).Exec(fileId)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
// delete tag on file
|
||||
_, err = tx.Stmt(database.stmt.deleteFileReferenceInFileHasTag).Exec(fileId)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
// delete reviews on file
|
||||
_, err = tx.Stmt(database.stmt.deleteFileReferenceInReviews).Exec(fileId)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
// commit transaction
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (database *Database) UpdateFilename(fileId int64, filename string) error {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
_, err := database.stmt.updateFilename.Exec(filename, fileId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (database *Database) ResetFilename(fileId int64) error {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
_, err := database.stmt.resetFilename.Exec(fileId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (database *Database) ResetFoldername(folderId int64) error {
|
||||
folder, err := database.GetFolder(folderId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
foldername := filepath.Base(folder.Folder)
|
||||
err = database.UpdateFoldername(folderId, foldername)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
49
pkg/database/method_feedback.go
Normal file
49
pkg/database/method_feedback.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package database
|
||||
|
||||
func (database *Database) InsertFeedback(time int64, content string, userID int64, header string) error {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
_, err := database.stmt.insertFeedback.Exec(time, content, userID, header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (database *Database) GetFeedbacks() ([]*Feedback, error) {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
rows, err := database.stmt.getFeedbacks.Query()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
feedbacks := make([]*Feedback, 0)
|
||||
for rows.Next() {
|
||||
feedback := &Feedback{
|
||||
User: &User{},
|
||||
}
|
||||
err := rows.Scan(
|
||||
&feedback.ID, &feedback.Time, &feedback.Content, &feedback.Header,
|
||||
&feedback.User.ID, &feedback.User.Username, &feedback.User.Role, &feedback.User.Active, &feedback.User.AvatarId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
feedbacks = append(feedbacks, feedback)
|
||||
}
|
||||
return feedbacks, nil
|
||||
}
|
||||
|
||||
func (database *Database) DeleteFeedback(id int64) error {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
_, err := database.stmt.deleteFeedback.Exec(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
123
pkg/database/method_review.go
Normal file
123
pkg/database/method_review.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package database
|
||||
|
||||
func (database *Database) InsertReview(review *Review) error {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
_, err := database.stmt.insertReview.Exec(
|
||||
review.UserId,
|
||||
review.FileId,
|
||||
review.CreatedAt,
|
||||
review.Content)
|
||||
return err
|
||||
}
|
||||
|
||||
func (database *Database) GetReviewsOnFile(fileId int64) ([]*Review, error) {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
rows, err := database.stmt.getReviewsOnFile.Query(fileId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
reviews := make([]*Review, 0)
|
||||
for rows.Next() {
|
||||
review := &Review{
|
||||
User: &User{},
|
||||
File: &File{},
|
||||
}
|
||||
err := rows.Scan(
|
||||
&review.ID,
|
||||
&review.CreatedAt,
|
||||
&review.UpdatedAt,
|
||||
&review.Content,
|
||||
&review.User.ID,
|
||||
&review.User.Username,
|
||||
&review.User.Role,
|
||||
&review.User.AvatarId,
|
||||
&review.File.ID,
|
||||
&review.File.Filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reviews = append(reviews, review)
|
||||
}
|
||||
return reviews, nil
|
||||
}
|
||||
|
||||
func (database *Database) GetReview(reviewId int64) (*Review, error) {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
row := database.stmt.getReview.QueryRow(reviewId)
|
||||
|
||||
review := &Review{}
|
||||
err := row.Scan(
|
||||
&review.ID,
|
||||
&review.FileId,
|
||||
&review.UserId,
|
||||
&review.CreatedAt,
|
||||
&review.UpdatedAt,
|
||||
&review.Content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return review, nil
|
||||
}
|
||||
|
||||
func (database *Database) UpdateReview(review *Review) error {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
_, err := database.stmt.updateReview.Exec(
|
||||
review.Content,
|
||||
review.UpdatedAt,
|
||||
review.ID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (database *Database) DeleteReview(reviewId int64) error {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
_, err := database.stmt.deleteReview.Exec(reviewId)
|
||||
return err
|
||||
}
|
||||
|
||||
func (database *Database) GetReviewsByUser(userId int64) ([]*Review, error) {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
rows, err := database.stmt.getReviewsByUser.Query(userId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
reviews := make([]*Review, 0)
|
||||
for rows.Next() {
|
||||
review := &Review{
|
||||
User: &User{},
|
||||
File: &File{},
|
||||
}
|
||||
err := rows.Scan(
|
||||
&review.ID,
|
||||
&review.CreatedAt,
|
||||
&review.UpdatedAt,
|
||||
&review.Content,
|
||||
&review.User.ID,
|
||||
&review.User.Username,
|
||||
&review.User.Role,
|
||||
&review.User.AvatarId,
|
||||
&review.File.ID,
|
||||
&review.File.Filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reviews = append(reviews, review)
|
||||
}
|
||||
return reviews, nil
|
||||
}
|
||||
110
pkg/database/method_tag.go
Normal file
110
pkg/database/method_tag.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package database
|
||||
|
||||
import "errors"
|
||||
|
||||
func (database *Database) InsertTag(tag *Tag) (int64, error) {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
result, err := database.stmt.insertTag.Exec(tag.Name, tag.Description, tag.CreatedByUserId)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (database *Database) GetTag(id int64) (*Tag, error) {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
tag := &Tag{CreatedByUser: &User{}}
|
||||
err := database.stmt.getTag.QueryRow(id).Scan(
|
||||
&tag.ID, &tag.Name, &tag.Description,
|
||||
&tag.CreatedByUser.ID, &tag.CreatedByUser.Username, &tag.CreatedByUser.Role, &tag.CreatedByUser.AvatarId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tag, nil
|
||||
}
|
||||
|
||||
func (database *Database) GetTags() ([]*Tag, error) {
|
||||
tags := []*Tag{}
|
||||
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
rows, err := database.stmt.getTags.Query()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
tag := &Tag{CreatedByUser: &User{}}
|
||||
err := rows.Scan(
|
||||
&tag.ID, &tag.Name, &tag.Description,
|
||||
&tag.CreatedByUser.ID, &tag.CreatedByUser.Username, &tag.CreatedByUser.Role, &tag.CreatedByUser.AvatarId)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func (database *Database) UpdateTag(tag *Tag) error {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
result, err := database.stmt.updateTag.Exec(tag.Name, tag.Description, tag.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
return errors.New("No rows affected")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// delete tag and all its references in file_has_tag
|
||||
func (database *Database) DeleteTag(id int64) error {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
// begin transaction
|
||||
tx, err := database.sqlConn.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// delete tag
|
||||
_, err = tx.Stmt(database.stmt.deleteTag).Exec(id)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
// delete file_has_tag
|
||||
_, err = tx.Stmt(database.stmt.deleteTagReferenceInFileHasTag).Exec(id)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
// commit transaction
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
48
pkg/database/method_tag_and_file.go
Normal file
48
pkg/database/method_tag_and_file.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package database
|
||||
|
||||
func (database *Database) PutTagOnFile(tagID, fileID, userID int64) error {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
_, err := database.stmt.putTagOnFile.Exec(tagID, fileID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (database *Database) GetTagsOnFile(fileID int64) ([]*Tag, error) {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
rows, err := database.stmt.getTagsOnFile.Query(fileID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
tags := make([]*Tag, 0)
|
||||
for rows.Next() {
|
||||
tag := &Tag{}
|
||||
err = rows.Scan(&tag.ID, &tag.Name, &tag.Description, &tag.CreatedByUserId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func (database *Database) DeleteTagOnFile(tagID, fileID int64) error {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
result, err := database.stmt.deleteTagOnFile.Exec(tagID, fileID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rows, _ := result.RowsAffected(); rows == 0 {
|
||||
return ErrTagNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
171
pkg/database/method_user.go
Normal file
171
pkg/database/method_user.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"log"
|
||||
)
|
||||
|
||||
func (database *Database) Login(username string, password string) (*User, error) {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
user := &User{}
|
||||
|
||||
// get user from database
|
||||
err := database.stmt.getUser.QueryRow(username).Scan(&user.ID, &user.Username, &user.Password, &user.Role, &user.Active, &user.AvatarId)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
// validate password
|
||||
err = database.ComparePassword(user.Password, password)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (database *Database) LoginAsAnonymous() (*User, error) {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
user := &User{}
|
||||
|
||||
// get user from database
|
||||
err := database.stmt.getAnonymousUser.QueryRow().Scan(&user.ID, &user.Username, &user.Role, &user.AvatarId)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (database *Database) Register(username string, password string, usertype int64) error {
|
||||
countAdmin, err := database.CountAdmin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
active := false
|
||||
if countAdmin == 0 {
|
||||
active = true
|
||||
}
|
||||
|
||||
// active normal user by default
|
||||
if usertype == 2 {
|
||||
active = true
|
||||
}
|
||||
|
||||
// encrypt password
|
||||
password = database.EncryptPassword(password)
|
||||
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
_, err = database.stmt.insertUser.Exec(username, password, usertype, active, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (database *Database) GetUserById(id int64) (*User, error) {
|
||||
user := &User{}
|
||||
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
// get user from database
|
||||
err := database.stmt.getUserById.QueryRow(id).Scan(&user.ID, &user.Username, &user.Role, &user.Active, &user.AvatarId)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (database *Database) CountAdmin() (int64, error) {
|
||||
var count int64
|
||||
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
err := database.stmt.countAdmin.QueryRow().Scan(&count)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (database *Database) GetUsers() ([]*User, error) {
|
||||
users := make([]*User, 0)
|
||||
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
rows, err := database.stmt.getUsers.Query()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
user := &User{}
|
||||
err = rows.Scan(&user.ID, &user.Username, &user.Role, &user.Active, &user.AvatarId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
users = append(users, user)
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (database *Database) UpdateUserActive(id int64, active bool) error {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
_, err := database.stmt.updateUserActive.Exec(active, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (database *Database) UpdateUsername(id int64, username string) error {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
_, err := database.stmt.updateUsername.Exec(username, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (database *Database) UpdateUserPassword(id int64, password string) error {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
// encrypt password
|
||||
password = database.EncryptPassword(password)
|
||||
|
||||
_, err := database.stmt.updateUserPassword.Exec(password, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (database *Database) EncryptPassword(password string) string {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.MinCost)
|
||||
if err != nil {
|
||||
log.Println("[database] Failed to hash password, using plaintext password")
|
||||
return password
|
||||
}
|
||||
|
||||
return string(hash)
|
||||
}
|
||||
|
||||
func (database *Database) ComparePassword(hashedPassword string, plainTextPassword string) error {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(plainTextPassword))
|
||||
return err
|
||||
}
|
||||
@@ -7,8 +7,10 @@ import (
|
||||
var initFilesTableQuery = `CREATE TABLE IF NOT EXISTS files (
|
||||
id INTEGER PRIMARY KEY,
|
||||
folder_id INTEGER NOT NULL,
|
||||
realname TEXT NOT NULL,
|
||||
filename TEXT NOT NULL,
|
||||
filesize INTEGER NOT NULL
|
||||
filesize INTEGER NOT NULL,
|
||||
FOREIGN KEY(folder_id) REFERENCES folders(id)
|
||||
);`
|
||||
|
||||
var initFoldersTableQuery = `CREATE TABLE IF NOT EXISTS folders (
|
||||
@@ -20,25 +22,111 @@ var initFoldersTableQuery = `CREATE TABLE IF NOT EXISTS folders (
|
||||
var initFeedbacksTableQuery = `CREATE TABLE IF NOT EXISTS feedbacks (
|
||||
id INTEGER PRIMARY KEY,
|
||||
time INTEGER NOT NULL,
|
||||
feedback TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
header TEXT NOT NULL
|
||||
);`
|
||||
|
||||
// User table schema definition
|
||||
// role: 0 - Anonymous User, 1 - Admin, 2 - User
|
||||
var initUsersTableQuery = `CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password TEXT NOT NULL,
|
||||
role INTEGER NOT NULL,
|
||||
active BOOLEAN NOT NULL,
|
||||
avatar_id INTEGER NOT NULL,
|
||||
FOREIGN KEY(avatar_id) REFERENCES avatars(id)
|
||||
);`
|
||||
|
||||
var initAvatarsTableQuery = `CREATE TABLE IF NOT EXISTS avatars (
|
||||
id INTEGER PRIMARY KEY,
|
||||
avatarname TEXT NOT NULL,
|
||||
avatar BLOB NOT NULL
|
||||
);`
|
||||
|
||||
var initTagsTableQuery = `CREATE TABLE IF NOT EXISTS tags (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT NOT NULL,
|
||||
created_by_user_id INTEGER NOT NULL,
|
||||
FOREIGN KEY(created_by_user_id) REFERENCES users(id)
|
||||
);`
|
||||
|
||||
var initFileHasTagTableQuery = `CREATE TABLE IF NOT EXISTS file_has_tag (
|
||||
file_id INTEGER NOT NULL,
|
||||
tag_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (file_id, tag_id),
|
||||
FOREIGN KEY(user_id) REFERENCES users(id)
|
||||
FOREIGN KEY (file_id) REFERENCES files(id),
|
||||
FOREIGN KEY (tag_id) REFERENCES tags(id)
|
||||
);`
|
||||
|
||||
var initLikesTableQuery = `CREATE TABLE IF NOT EXISTS likes (
|
||||
user_id INTEGER NOT NULL,
|
||||
file_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (user_id, file_id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (file_id) REFERENCES files(id)
|
||||
);`
|
||||
|
||||
var initReviewsTableQuery = `CREATE TABLE IF NOT EXISTS reviews (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
file_id INTEGER NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL DEFAULT 0,
|
||||
content TEXT NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (file_id) REFERENCES files(id)
|
||||
);`
|
||||
|
||||
var initPlaybacksTableQuery = `CREATE TABLE IF NOT EXISTS playbacks (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
file_id INTEGER NOT NULL,
|
||||
time INTEGER NOT NULL,
|
||||
mothod INTEGER NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (file_id) REFERENCES files(id)
|
||||
);`
|
||||
|
||||
var initLogsTableQuery = `CREATE TABLE IF NOT EXISTS logs (
|
||||
id INTEGER PRIMARY KEY,
|
||||
time INTEGER NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);`
|
||||
|
||||
var initTmpfsTableQuery = `CREATE TABLE IF NOT EXISTS tmpfs (
|
||||
id INTEGER PRIMARY KEY,
|
||||
path TEXT NOT NULL,
|
||||
size INTEGER NOT NULL,
|
||||
file_id INTEGER NOT NULL,
|
||||
ffmpeg_config TEXT NOT NULL,
|
||||
created_time INTEGER NOT NULL,
|
||||
accessed_time INTEGER NOT NULL,
|
||||
FOREIGN KEY (file_id) REFERENCES files(id)
|
||||
);`
|
||||
|
||||
var insertFolderQuery = `INSERT INTO folders (folder, foldername)
|
||||
VALUES (?, ?);`
|
||||
|
||||
var findFolderQuery = `SELECT id FROM folders WHERE folder = ? LIMIT 1;`
|
||||
|
||||
var findFileQuery = `SELECT id FROM files WHERE folder_id = ? AND filename = ? LIMIT 1;`
|
||||
var findFileQuery = `SELECT id FROM files WHERE folder_id = ? AND realname = ? LIMIT 1;`
|
||||
|
||||
var insertFileQuery = `INSERT INTO files (folder_id, filename, filesize)
|
||||
VALUES (?, ?, ?);`
|
||||
var insertFileQuery = `INSERT INTO files (folder_id, realname, filename, filesize)
|
||||
VALUES (?, ?, ?, ?);`
|
||||
|
||||
var searchFilesQuery = `SELECT
|
||||
files.id, files.folder_id, files.filename, folders.foldername, files.filesize
|
||||
FROM files
|
||||
JOIN folders ON files.folder_id = folders.id
|
||||
WHERE filename LIKE ?
|
||||
ORDER BY folders.foldername, files.filename
|
||||
LIMIT ? OFFSET ?;`
|
||||
|
||||
var getFolderQuery = `SELECT folder FROM folders WHERE id = ? LIMIT 1;`
|
||||
@@ -48,7 +136,7 @@ var dropFilesQuery = `DROP TABLE files;`
|
||||
var dropFolderQuery = `DROP TABLE folders;`
|
||||
|
||||
var getFileQuery = `SELECT
|
||||
files.id, files.folder_id, files.filename, folders.foldername, files.filesize
|
||||
files.id, files.folder_id, files.realname, files.filename, folders.foldername, files.filesize
|
||||
FROM files
|
||||
JOIN folders ON files.folder_id = folders.id
|
||||
WHERE files.id = ?
|
||||
@@ -58,6 +146,7 @@ var searchFoldersQuery = `SELECT
|
||||
id, folder, foldername
|
||||
FROM folders
|
||||
WHERE foldername LIKE ?
|
||||
ORDER BY foldername
|
||||
LIMIT ? OFFSET ?;`
|
||||
|
||||
var getFilesInFolderQuery = `SELECT
|
||||
@@ -65,6 +154,7 @@ files.id, files.filename, files.filesize, folders.foldername
|
||||
FROM files
|
||||
JOIN folders ON files.folder_id = folders.id
|
||||
WHERE folder_id = ?
|
||||
ORDER BY files.filename
|
||||
LIMIT ? OFFSET ?;`
|
||||
|
||||
var getRandomFilesQuery = `SELECT
|
||||
@@ -74,13 +164,140 @@ JOIN folders ON files.folder_id = folders.id
|
||||
ORDER BY RANDOM()
|
||||
LIMIT ?;`
|
||||
|
||||
var insertFeedbackQuery = `INSERT INTO feedbacks (time, feedback, header)
|
||||
VALUES (?, ?, ?);`
|
||||
var getRandomFilesWithTagQuery = `SELECT
|
||||
files.id, files.folder_id, files.filename, folders.foldername, files.filesize
|
||||
FROM file_has_tag
|
||||
JOIN files ON file_has_tag.file_id = files.id
|
||||
JOIN folders ON files.folder_id = folders.id
|
||||
WHERE file_has_tag.tag_id = ?
|
||||
ORDER BY RANDOM()
|
||||
LIMIT ?;`
|
||||
|
||||
var insertFeedbackQuery = `INSERT INTO feedbacks (time, content, user_id, header)
|
||||
VALUES (?, ?, ?, ?);`
|
||||
|
||||
var getFeedbacksQuery = `SELECT
|
||||
feedbacks.id, feedbacks.time, feedbacks.content, feedbacks.header,
|
||||
users.id, users.username, users.role, users.active, users.avatar_id
|
||||
FROM feedbacks
|
||||
JOIN users ON feedbacks.user_id = users.id
|
||||
ORDER BY feedbacks.time
|
||||
;`
|
||||
|
||||
var deleteFeedbackQuery = `DELETE FROM feedbacks WHERE id = ?;`
|
||||
|
||||
var insertUserQuery = `INSERT INTO users (username, password, role, active, avatar_id)
|
||||
VALUES (?, ?, ?, ?, ?);`
|
||||
|
||||
var countUserQuery = `SELECT count(*) FROM users;`
|
||||
|
||||
var countAdminQuery = `SELECT count(*) FROM users WHERE role= 1;`
|
||||
|
||||
var getUserQuery = `SELECT id, username, password, role, active, avatar_id FROM users WHERE username = ? LIMIT 1;`
|
||||
|
||||
var getUsersQuery = `SELECT id, username, role, active, avatar_id FROM users;`
|
||||
|
||||
var getUserByIdQuery = `SELECT id, username, role, active, avatar_id FROM users WHERE id = ? LIMIT 1;`
|
||||
|
||||
var updateUserActiveQuery = `UPDATE users SET active = ? WHERE id = ?;`
|
||||
|
||||
var updateUsernameQuery = `UPDATE users SET username = ? WHERE id = ?;`
|
||||
|
||||
var updateUserPasswordQuery = `UPDATE users SET password = ? WHERE id = ?;`
|
||||
|
||||
var getAnonymousUserQuery = `SELECT id, username, role, avatar_id FROM users WHERE role = 0 LIMIT 1;`
|
||||
|
||||
var insertTagQuery = `INSERT INTO tags (name, description, created_by_user_id) VALUES (?, ?, ?);`
|
||||
|
||||
var deleteTagQuery = `DELETE FROM tags WHERE id = ?;`
|
||||
|
||||
var getTagQuery = `SELECT
|
||||
tags.id, tags.name, tags.description,
|
||||
users.id, users.username, users.role, users.avatar_id
|
||||
FROM tags
|
||||
JOIN users ON tags.created_by_user_id = users.id
|
||||
WHERE tags.id = ? LIMIT 1;`
|
||||
|
||||
var getTagsQuery = `SELECT
|
||||
tags.id, tags.name, tags.description,
|
||||
users.id, users.username, users.role, users.avatar_id
|
||||
FROM tags
|
||||
JOIN users ON tags.created_by_user_id = users.id
|
||||
ORDER BY tags.name
|
||||
;`
|
||||
|
||||
var updateTagQuery = `UPDATE tags SET name = ?, description = ? WHERE id = ?;`
|
||||
|
||||
var putTagOnFileQuery = `INSERT OR IGNORE INTO file_has_tag (tag_id, file_id, user_id) VALUES (?, ?, ?);`
|
||||
|
||||
var getTagsOnFileQuery = `SELECT
|
||||
tags.id, tags.name, tags.description, tags.created_by_user_id
|
||||
FROM file_has_tag
|
||||
JOIN tags ON file_has_tag.tag_id = tags.id
|
||||
WHERE file_has_tag.file_id = ?
|
||||
ORDER BY tags.name
|
||||
;`
|
||||
|
||||
var deleteTagOnFileQuery = `DELETE FROM file_has_tag WHERE tag_id = ? AND file_id = ?;`
|
||||
|
||||
var deleteTagReferenceInFileHasTagQuery = `DELETE FROM file_has_tag WHERE tag_id = ?;`
|
||||
|
||||
var updateFoldernameQuery = `UPDATE folders SET foldername = ? WHERE id = ?;`
|
||||
|
||||
var insertReviewQuery = `INSERT INTO reviews (user_id, file_id, created_at, content)
|
||||
VALUES (?, ?, ?, ?);`
|
||||
|
||||
var getReviewsOnFileQuery = `SELECT
|
||||
reviews.id, reviews.created_at, reviews.updated_at, reviews.content,
|
||||
users.id, users.username, users.role, users.avatar_id,
|
||||
files.id, files.filename
|
||||
FROM reviews
|
||||
JOIN users ON reviews.user_id = users.id
|
||||
JOIN files ON reviews.file_id = files.id
|
||||
WHERE reviews.file_id = ?
|
||||
ORDER BY reviews.created_at
|
||||
;`
|
||||
|
||||
var getReviewQuery = `SELECT id, file_id, user_id, created_at, updated_at, content FROM reviews WHERE id = ? LIMIT 1;`
|
||||
|
||||
var updateReviewQuery = `UPDATE reviews SET content = ?, updated_at = ? WHERE id = ?;`
|
||||
|
||||
var deleteReviewQuery = `DELETE FROM reviews WHERE id = ?;`
|
||||
|
||||
var getReviewsByUserQuery = `SELECT
|
||||
reviews.id, reviews.created_at, reviews.updated_at, reviews.content,
|
||||
users.id, users.username, users.role, users.avatar_id,
|
||||
files.id, files.filename
|
||||
FROM reviews
|
||||
JOIN users ON reviews.user_id = users.id
|
||||
JOIN files ON reviews.file_id = files.id
|
||||
WHERE reviews.user_id = ?
|
||||
ORDER BY reviews.created_at
|
||||
;`
|
||||
|
||||
var deleteFileQuery = `DELETE FROM files WHERE id = ?;`
|
||||
|
||||
var deleteFileReferenceInFileHasTagQuery = `DELETE FROM file_has_tag WHERE file_id = ?;`
|
||||
|
||||
var deleteFileReferenceInReviewsQuery = `DELETE FROM reviews WHERE file_id = ?;`
|
||||
|
||||
var updateFilenameQuery = `UPDATE files SET filename = ? WHERE id = ?;`
|
||||
|
||||
var resetFilenameQuery = `UPDATE files SET filename = realname WHERE id = ?;`
|
||||
|
||||
type Stmt struct {
|
||||
initFilesTable *sql.Stmt
|
||||
initFoldersTable *sql.Stmt
|
||||
initFeedbacksTable *sql.Stmt
|
||||
initUsersTable *sql.Stmt
|
||||
initAvatarsTable *sql.Stmt
|
||||
initTagsTable *sql.Stmt
|
||||
initFileHasTag *sql.Stmt
|
||||
initLikesTable *sql.Stmt
|
||||
initReviewsTable *sql.Stmt
|
||||
initPlaybacksTable *sql.Stmt
|
||||
initLogsTable *sql.Stmt
|
||||
initTmpfsTable *sql.Stmt
|
||||
insertFolder *sql.Stmt
|
||||
insertFile *sql.Stmt
|
||||
findFolder *sql.Stmt
|
||||
@@ -93,7 +310,41 @@ type Stmt struct {
|
||||
searchFolders *sql.Stmt
|
||||
getFilesInFolder *sql.Stmt
|
||||
getRandomFiles *sql.Stmt
|
||||
getRandomFilesWithTag *sql.Stmt
|
||||
insertFeedback *sql.Stmt
|
||||
getFeedbacks *sql.Stmt
|
||||
deleteFeedback *sql.Stmt
|
||||
insertUser *sql.Stmt
|
||||
countUser *sql.Stmt
|
||||
countAdmin *sql.Stmt
|
||||
getUser *sql.Stmt
|
||||
getUsers *sql.Stmt
|
||||
getUserById *sql.Stmt
|
||||
updateUserActive *sql.Stmt
|
||||
updateUsername *sql.Stmt
|
||||
updateUserPassword *sql.Stmt
|
||||
getAnonymousUser *sql.Stmt
|
||||
insertTag *sql.Stmt
|
||||
deleteTag *sql.Stmt
|
||||
getTag *sql.Stmt
|
||||
getTags *sql.Stmt
|
||||
updateTag *sql.Stmt
|
||||
putTagOnFile *sql.Stmt
|
||||
getTagsOnFile *sql.Stmt
|
||||
deleteTagOnFile *sql.Stmt
|
||||
deleteTagReferenceInFileHasTag *sql.Stmt
|
||||
updateFoldername *sql.Stmt
|
||||
insertReview *sql.Stmt
|
||||
getReviewsOnFile *sql.Stmt
|
||||
getReview *sql.Stmt
|
||||
updateReview *sql.Stmt
|
||||
deleteReview *sql.Stmt
|
||||
getReviewsByUser *sql.Stmt
|
||||
deleteFile *sql.Stmt
|
||||
deleteFileReferenceInFileHasTag *sql.Stmt
|
||||
deleteFileReferenceInReviews *sql.Stmt
|
||||
updateFilename *sql.Stmt
|
||||
resetFilename *sql.Stmt
|
||||
}
|
||||
|
||||
func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) {
|
||||
@@ -119,6 +370,60 @@ func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init users table
|
||||
stmt.initUsersTable, err = sqlConn.Prepare(initUsersTableQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init avatars table
|
||||
stmt.initAvatarsTable, err = sqlConn.Prepare(initAvatarsTableQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init tags table
|
||||
stmt.initTagsTable, err = sqlConn.Prepare(initTagsTableQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init file_has_tag table
|
||||
stmt.initFileHasTag, err = sqlConn.Prepare(initFileHasTagTableQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init likes table
|
||||
stmt.initLikesTable, err = sqlConn.Prepare(initLikesTableQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init reviews table
|
||||
stmt.initReviewsTable, err = sqlConn.Prepare(initReviewsTableQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init playbacks table
|
||||
stmt.initPlaybacksTable, err = sqlConn.Prepare(initPlaybacksTableQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init logs table
|
||||
stmt.initLogsTable, err = sqlConn.Prepare(initLogsTableQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init tmpfs table
|
||||
stmt.initTmpfsTable, err = sqlConn.Prepare(initTmpfsTableQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// run init statement
|
||||
_, err = stmt.initFilesTable.Exec()
|
||||
if err != nil {
|
||||
@@ -132,6 +437,42 @@ func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = stmt.initUsersTable.Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = stmt.initAvatarsTable.Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = stmt.initTagsTable.Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = stmt.initFileHasTag.Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = stmt.initLikesTable.Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = stmt.initReviewsTable.Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = stmt.initPlaybacksTable.Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = stmt.initLogsTable.Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = stmt.initTmpfsTable.Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init insert folder statement
|
||||
stmt.insertFolder, err = sqlConn.Prepare(insertFolderQuery)
|
||||
@@ -205,11 +546,231 @@ func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init getRandomFilesWithTag
|
||||
stmt.getRandomFilesWithTag, err = sqlConn.Prepare(getRandomFilesWithTagQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init insertFeedback
|
||||
stmt.insertFeedback, err = sqlConn.Prepare(insertFeedbackQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init getFeedbacks
|
||||
stmt.getFeedbacks, err = sqlConn.Prepare(getFeedbacksQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init deleteFeedback
|
||||
stmt.deleteFeedback, err = sqlConn.Prepare(deleteFeedbackQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init insertUser
|
||||
stmt.insertUser, err = sqlConn.Prepare(insertUserQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init countUser
|
||||
stmt.countUser, err = sqlConn.Prepare(countUserQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init countAdmin
|
||||
stmt.countAdmin, err = sqlConn.Prepare(countAdminQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init getUser
|
||||
stmt.getUser, err = sqlConn.Prepare(getUserQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init getUsers
|
||||
stmt.getUsers, err = sqlConn.Prepare(getUsersQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init getUserById
|
||||
stmt.getUserById, err = sqlConn.Prepare(getUserByIdQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init updateUserActive
|
||||
stmt.updateUserActive, err = sqlConn.Prepare(updateUserActiveQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init updateUsername
|
||||
stmt.updateUsername, err = sqlConn.Prepare(updateUsernameQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init updateUserPassword
|
||||
stmt.updateUserPassword, err = sqlConn.Prepare(updateUserPasswordQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init getAnonymousUser
|
||||
stmt.getAnonymousUser, err = sqlConn.Prepare(getAnonymousUserQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// insert Anonymous user if users is empty
|
||||
userCount := 0
|
||||
err = stmt.countUser.QueryRow().Scan(&userCount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if userCount == 0 {
|
||||
_, err = stmt.insertUser.Exec("Anonymous user", "", 0, 1, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// init insertTag
|
||||
stmt.insertTag, err = sqlConn.Prepare(insertTagQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init deleteTag
|
||||
stmt.deleteTag, err = sqlConn.Prepare(deleteTagQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init getTag
|
||||
stmt.getTag, err = sqlConn.Prepare(getTagQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init getTags
|
||||
stmt.getTags, err = sqlConn.Prepare(getTagsQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init updateTag
|
||||
stmt.updateTag, err = sqlConn.Prepare(updateTagQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init putTagOnFile
|
||||
stmt.putTagOnFile, err = sqlConn.Prepare(putTagOnFileQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init getTagsOnFile
|
||||
stmt.getTagsOnFile, err = sqlConn.Prepare(getTagsOnFileQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init deleteTagOnFile
|
||||
stmt.deleteTagOnFile, err = sqlConn.Prepare(deleteTagOnFileQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init deleteTagReferenceInFileHasTag
|
||||
stmt.deleteTagReferenceInFileHasTag, err = sqlConn.Prepare(
|
||||
deleteTagReferenceInFileHasTagQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init updateFoldername
|
||||
stmt.updateFoldername, err = sqlConn.Prepare(updateFoldernameQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init insertReview
|
||||
stmt.insertReview, err = sqlConn.Prepare(insertReviewQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init getReviewsOnFile
|
||||
stmt.getReviewsOnFile, err = sqlConn.Prepare(getReviewsOnFileQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init getReview
|
||||
stmt.getReview, err = sqlConn.Prepare(getReviewQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init updateReview
|
||||
stmt.updateReview, err = sqlConn.Prepare(updateReviewQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init deleteReview
|
||||
stmt.deleteReview, err = sqlConn.Prepare(deleteReviewQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init getReviewsByUser
|
||||
stmt.getReviewsByUser, err = sqlConn.Prepare(getReviewsByUserQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init deleteFile
|
||||
stmt.deleteFile, err = sqlConn.Prepare(deleteFileQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init deleteFileReferenceInFileHasTag
|
||||
stmt.deleteFileReferenceInFileHasTag, err = sqlConn.Prepare(
|
||||
deleteFileReferenceInFileHasTagQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init deleteFileReferenceInReviews
|
||||
stmt.deleteFileReferenceInReviews, err = sqlConn.Prepare(
|
||||
deleteFileReferenceInReviewsQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init updateFilename
|
||||
stmt.updateFilename, err = sqlConn.Prepare(updateFilenameQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init resetFilename
|
||||
stmt.resetFilename, err = sqlConn.Prepare(resetFilenameQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stmt, err
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ type File struct {
|
||||
ID int64 `json:"id"`
|
||||
Folder_id int64 `json:"folder_id"`
|
||||
Foldername string `json:"foldername"`
|
||||
Realname string `json:"-"`
|
||||
Filename string `json:"filename"`
|
||||
Filesize int64 `json:"filesize"`
|
||||
}
|
||||
@@ -20,11 +21,53 @@ type Folder struct {
|
||||
Foldername string `json:"foldername"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID int64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"-"`
|
||||
Role int64 `json:"role"`
|
||||
Active bool `json:"active"`
|
||||
AvatarId int64 `json:"avatar_id"`
|
||||
}
|
||||
|
||||
type Tag struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
CreatedByUserId int64 `json:"created_by_user_id"`
|
||||
CreatedByUser *User `json:"created_by_user"`
|
||||
}
|
||||
|
||||
type Review struct {
|
||||
ID int64 `json:"id"`
|
||||
FileId int64 `json:"file_id"`
|
||||
File *File `json:"file"`
|
||||
UserId int64 `json:"user_id"`
|
||||
User *User `json:"user"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type Feedback struct {
|
||||
ID int64 `json:"id"`
|
||||
UserId int64 `json:"user_id"`
|
||||
User *User `json:"user"`
|
||||
Content string `json:"content"`
|
||||
Header string `json:"header"`
|
||||
Time int64 `json:"time"`
|
||||
}
|
||||
|
||||
var (
|
||||
RoleAnonymous = int64(0)
|
||||
RoleAdmin = int64(1)
|
||||
RoleUser = int64(2)
|
||||
)
|
||||
|
||||
func (f *File) Path() (string, error) {
|
||||
folder, err := f.Db.GetFolder(f.Folder_id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(folder.Folder, f.Filename), nil
|
||||
return filepath.Join(folder.Folder, f.Realname), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package tmpfs
|
||||
|
||||
import (
|
||||
"log"
|
||||
"msw-open-music/pkg/commonconfig"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@@ -11,29 +12,42 @@ import (
|
||||
|
||||
type Tmpfs struct {
|
||||
record map[string]int64
|
||||
Config TmpfsConfig
|
||||
Config commonconfig.TmpfsConfig
|
||||
wg sync.WaitGroup
|
||||
recordLocks map[string]*sync.Mutex
|
||||
}
|
||||
|
||||
func (tmpfs *Tmpfs) GetObjFilePath(id int64, configName string) (string) {
|
||||
return filepath.Join(tmpfs.Config.Root, strconv.FormatInt(id, 10) + "." + configName + ".ogg")
|
||||
func (tmpfs *Tmpfs) GetObjFilePath(id int64, ffmpegConfig commonconfig.FfmpegConfig) string {
|
||||
return filepath.Join(tmpfs.Config.Root, strconv.FormatInt(id, 10)+"."+ffmpegConfig.Name+"."+ffmpegConfig.Format)
|
||||
}
|
||||
|
||||
type TmpfsConfig struct {
|
||||
FileLifeTime int64 `json:"file_life_time"`
|
||||
CleanerInternal int64 `json:"cleaner_internal"`
|
||||
Root string `json:"root"`
|
||||
func (tmpfs *Tmpfs) GetLock(filename string) *sync.Mutex {
|
||||
if _, ok := tmpfs.recordLocks[filename]; !ok {
|
||||
tmpfs.recordLocks[filename] = &sync.Mutex{}
|
||||
}
|
||||
return tmpfs.recordLocks[filename]
|
||||
}
|
||||
|
||||
func NewTmpfsConfig() (*TmpfsConfig) {
|
||||
config := &TmpfsConfig{}
|
||||
return config
|
||||
func (tmpfs *Tmpfs) Lock(filename string) {
|
||||
tmpfs.GetLock(filename).Lock()
|
||||
}
|
||||
|
||||
func NewTmpfs(config TmpfsConfig) *Tmpfs {
|
||||
func (tmpfs *Tmpfs) Unlock(filename string) {
|
||||
tmpfs.GetLock(filename).Unlock()
|
||||
}
|
||||
|
||||
func NewTmpfs(config commonconfig.TmpfsConfig) *Tmpfs {
|
||||
tmpfs := &Tmpfs{
|
||||
record: make(map[string]int64),
|
||||
Config: config,
|
||||
recordLocks: make(map[string]*sync.Mutex),
|
||||
}
|
||||
// check if the directory exists
|
||||
if _, err := os.Stat(tmpfs.Config.Root); os.IsNotExist(err) {
|
||||
err = os.MkdirAll(tmpfs.Config.Root, 0755)
|
||||
if err != nil {
|
||||
log.Fatalln("[tmpfs] Failed to create directory", tmpfs.Config.Root)
|
||||
}
|
||||
}
|
||||
tmpfs.wg.Add(1)
|
||||
go tmpfs.Cleaner()
|
||||
@@ -44,7 +58,7 @@ func (tmpfs *Tmpfs) Record(filename string) {
|
||||
tmpfs.record[filename] = time.Now().Unix()
|
||||
}
|
||||
|
||||
func (tmpfs *Tmpfs) Exits(filename string) (bool) {
|
||||
func (tmpfs *Tmpfs) Exits(filename string) bool {
|
||||
_, ok := tmpfs.record[filename]
|
||||
return ok
|
||||
}
|
||||
@@ -53,15 +67,23 @@ func (tmpfs *Tmpfs) Cleaner() {
|
||||
var err error
|
||||
for {
|
||||
now := time.Now().Unix()
|
||||
for key, value := range tmpfs.record {
|
||||
if now - value > tmpfs.Config.FileLifeTime {
|
||||
err = os.Remove(key)
|
||||
for path, lock := range tmpfs.recordLocks {
|
||||
lock.Lock()
|
||||
recordTime, ok := tmpfs.record[path]
|
||||
if !ok {
|
||||
lock.Unlock()
|
||||
continue
|
||||
}
|
||||
if now-recordTime > tmpfs.Config.FileLifeTime {
|
||||
err = os.Remove(path)
|
||||
if err != nil {
|
||||
log.Println("[tmpfs] Failed to remove file", err)
|
||||
}
|
||||
log.Println("[tmpfs] Deleted file", key)
|
||||
delete(tmpfs.record, key)
|
||||
log.Println("[tmpfs] Deleted file", path)
|
||||
delete(tmpfs.record, path)
|
||||
delete(tmpfs.recordLocks, path)
|
||||
}
|
||||
lock.Unlock()
|
||||
}
|
||||
|
||||
time.Sleep(time.Second)
|
||||
|
||||
111
web/package-lock.json
generated
111
web/package-lock.json
generated
@@ -13,9 +13,9 @@
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-router": "^6.0.2",
|
||||
"react-router-dom": "^6.0.2",
|
||||
"react-scripts": "4.0.3",
|
||||
"react-router": "^6.3.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-scripts": "^4.0.3",
|
||||
"water.css": "^2.1.1",
|
||||
"web-vitals": "^1.1.2"
|
||||
},
|
||||
@@ -5687,13 +5687,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001280",
|
||||
"resolved": "https://registry.npmmirror.com/caniuse-lite/download/caniuse-lite-1.0.30001280.tgz?cache=0&sync_timestamp=1636700110747&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fcaniuse-lite%2Fdownload%2Fcaniuse-lite-1.0.30001280.tgz",
|
||||
"integrity": "sha512-kFXwYvHe5rix25uwueBxC569o53J6TpnGu0BEEn+6Lhl2vsnAumRFWEBhDft1fwyo6m1r4i+RqA4+163FpeFcA==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
}
|
||||
"version": "1.0.30001352",
|
||||
"resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001352.tgz",
|
||||
"integrity": "sha512-GUgH8w6YergqPQDGWhJGt8GDRnY0L/iJVQcU3eJ46GYf52R8tk0Wxp0PymuFVZboJYXGiCqwozAYZNRjVj6IcA=="
|
||||
},
|
||||
"node_modules/capture-exit": {
|
||||
"version": "2.0.0",
|
||||
@@ -9672,10 +9668,9 @@
|
||||
"integrity": "sha1-TAb8y0YC/iYCs8k9+C1+fb8aio4="
|
||||
},
|
||||
"node_modules/history": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/history/download/history-5.1.0.tgz",
|
||||
"integrity": "sha1-LpPAnAZBlNONUu1ir9CvydmwHs4=",
|
||||
"license": "MIT",
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/history/-/history-5.3.0.tgz",
|
||||
"integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.7.6"
|
||||
}
|
||||
@@ -15707,8 +15702,8 @@
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/react/download/react-17.0.2.tgz?cache=0&sync_timestamp=1636647716894&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Freact%2Fdownload%2Freact-17.0.2.tgz",
|
||||
"integrity": "sha1-0LXMUW0p6z7uOD91tihkz7aAADc=",
|
||||
"resolved": "https://registry.npmmirror.com/react/-/react-17.0.2.tgz",
|
||||
"integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1"
|
||||
@@ -15848,8 +15843,8 @@
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/react-dom/download/react-dom-17.0.2.tgz",
|
||||
"integrity": "sha1-7P+2hF462Nv83EmPDQqTlzZQLCM=",
|
||||
"resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-17.0.2.tgz",
|
||||
"integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
@@ -15878,23 +15873,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/react-router/download/react-router-6.0.2.tgz",
|
||||
"integrity": "sha512-8/Wm3Ed8t7TuedXjAvV39+c8j0vwrI5qVsYqjFr5WkJjsJpEvNSoLRUbtqSEYzqaTUj1IV+sbPJxvO+accvU0Q==",
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/react-router/-/react-router-6.3.0.tgz",
|
||||
"integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==",
|
||||
"dependencies": {
|
||||
"history": "^5.1.0"
|
||||
"history": "^5.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/react-router-dom/download/react-router-dom-6.0.2.tgz?cache=0&sync_timestamp=1636500332816&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Freact-router-dom%2Fdownload%2Freact-router-dom-6.0.2.tgz",
|
||||
"integrity": "sha512-cOpJ4B6raFutr0EG8O/M2fEoyQmwvZWomf1c6W2YXBZuFBx8oTk/zqjXghwScyhfrtnt0lANXV2182NQblRxFA==",
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-6.3.0.tgz",
|
||||
"integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==",
|
||||
"dependencies": {
|
||||
"history": "^5.1.0",
|
||||
"react-router": "6.0.2"
|
||||
"history": "^5.2.0",
|
||||
"react-router": "6.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8",
|
||||
@@ -15903,8 +15898,8 @@
|
||||
},
|
||||
"node_modules/react-scripts": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/react-scripts/download/react-scripts-4.0.3.tgz",
|
||||
"integrity": "sha1-scr+18P6YD52KLoPGHeHlky100U=",
|
||||
"resolved": "https://registry.npmmirror.com/react-scripts/-/react-scripts-4.0.3.tgz",
|
||||
"integrity": "sha512-S5eO4vjUzUisvkIPB7jVsKtuH2HhWcASREYWHAQ1FP5HyCv3xgn+wpILAEWkmy+A+tTNbSZClhxjT3qz6g4L1A==",
|
||||
"dependencies": {
|
||||
"@babel/core": "7.12.3",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "0.4.3",
|
||||
@@ -19445,8 +19440,8 @@
|
||||
},
|
||||
"node_modules/water.css": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/water.css/download/water.css-2.1.1.tgz",
|
||||
"integrity": "sha1-7m/oM6MTo6LttYilftYrPjHe3Rg="
|
||||
"resolved": "https://registry.npmmirror.com/water.css/-/water.css-2.1.1.tgz",
|
||||
"integrity": "sha512-gkO5byC+pZ7ndEV18hs/RmxKoDtEZXx06tZU4ocI3IBdv4xV64tlhjIFbDjurysRnNkiy2oQTr8PakRyzZWPJw=="
|
||||
},
|
||||
"node_modules/wbuf": {
|
||||
"version": "1.7.3",
|
||||
@@ -19458,8 +19453,8 @@
|
||||
},
|
||||
"node_modules/web-vitals": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/web-vitals/download/web-vitals-1.1.2.tgz",
|
||||
"integrity": "sha1-BlNTCBaJhgliOaqEcW5otMaubRw="
|
||||
"resolved": "https://registry.npmmirror.com/web-vitals/-/web-vitals-1.1.2.tgz",
|
||||
"integrity": "sha512-PFMKIY+bRSXlMxVAQ+m2aw9c/ioUYfDgrYot0YUa+/xa0sakubWhSDyxAKwzymvXVdF4CZI71g06W+mqhzu6ig=="
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "6.1.0",
|
||||
@@ -25227,9 +25222,9 @@
|
||||
}
|
||||
},
|
||||
"caniuse-lite": {
|
||||
"version": "1.0.30001280",
|
||||
"resolved": "https://registry.npmmirror.com/caniuse-lite/download/caniuse-lite-1.0.30001280.tgz?cache=0&sync_timestamp=1636700110747&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fcaniuse-lite%2Fdownload%2Fcaniuse-lite-1.0.30001280.tgz",
|
||||
"integrity": "sha512-kFXwYvHe5rix25uwueBxC569o53J6TpnGu0BEEn+6Lhl2vsnAumRFWEBhDft1fwyo6m1r4i+RqA4+163FpeFcA=="
|
||||
"version": "1.0.30001352",
|
||||
"resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001352.tgz",
|
||||
"integrity": "sha512-GUgH8w6YergqPQDGWhJGt8GDRnY0L/iJVQcU3eJ46GYf52R8tk0Wxp0PymuFVZboJYXGiCqwozAYZNRjVj6IcA=="
|
||||
},
|
||||
"capture-exit": {
|
||||
"version": "2.0.0",
|
||||
@@ -28381,9 +28376,9 @@
|
||||
"integrity": "sha1-TAb8y0YC/iYCs8k9+C1+fb8aio4="
|
||||
},
|
||||
"history": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/history/download/history-5.1.0.tgz",
|
||||
"integrity": "sha1-LpPAnAZBlNONUu1ir9CvydmwHs4=",
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/history/-/history-5.3.0.tgz",
|
||||
"integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.7.6"
|
||||
}
|
||||
@@ -33188,8 +33183,8 @@
|
||||
},
|
||||
"react": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/react/download/react-17.0.2.tgz?cache=0&sync_timestamp=1636647716894&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Freact%2Fdownload%2Freact-17.0.2.tgz",
|
||||
"integrity": "sha1-0LXMUW0p6z7uOD91tihkz7aAADc=",
|
||||
"resolved": "https://registry.npmmirror.com/react/-/react-17.0.2.tgz",
|
||||
"integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1"
|
||||
@@ -33303,8 +33298,8 @@
|
||||
},
|
||||
"react-dom": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/react-dom/download/react-dom-17.0.2.tgz",
|
||||
"integrity": "sha1-7P+2hF462Nv83EmPDQqTlzZQLCM=",
|
||||
"resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-17.0.2.tgz",
|
||||
"integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
@@ -33327,26 +33322,26 @@
|
||||
"integrity": "sha1-ch1GV2ctQAxePHXQY8SoX7LV1o8="
|
||||
},
|
||||
"react-router": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/react-router/download/react-router-6.0.2.tgz",
|
||||
"integrity": "sha512-8/Wm3Ed8t7TuedXjAvV39+c8j0vwrI5qVsYqjFr5WkJjsJpEvNSoLRUbtqSEYzqaTUj1IV+sbPJxvO+accvU0Q==",
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/react-router/-/react-router-6.3.0.tgz",
|
||||
"integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==",
|
||||
"requires": {
|
||||
"history": "^5.1.0"
|
||||
"history": "^5.2.0"
|
||||
}
|
||||
},
|
||||
"react-router-dom": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/react-router-dom/download/react-router-dom-6.0.2.tgz?cache=0&sync_timestamp=1636500332816&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Freact-router-dom%2Fdownload%2Freact-router-dom-6.0.2.tgz",
|
||||
"integrity": "sha512-cOpJ4B6raFutr0EG8O/M2fEoyQmwvZWomf1c6W2YXBZuFBx8oTk/zqjXghwScyhfrtnt0lANXV2182NQblRxFA==",
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-6.3.0.tgz",
|
||||
"integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==",
|
||||
"requires": {
|
||||
"history": "^5.1.0",
|
||||
"react-router": "6.0.2"
|
||||
"history": "^5.2.0",
|
||||
"react-router": "6.3.0"
|
||||
}
|
||||
},
|
||||
"react-scripts": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/react-scripts/download/react-scripts-4.0.3.tgz",
|
||||
"integrity": "sha1-scr+18P6YD52KLoPGHeHlky100U=",
|
||||
"resolved": "https://registry.npmmirror.com/react-scripts/-/react-scripts-4.0.3.tgz",
|
||||
"integrity": "sha512-S5eO4vjUzUisvkIPB7jVsKtuH2HhWcASREYWHAQ1FP5HyCv3xgn+wpILAEWkmy+A+tTNbSZClhxjT3qz6g4L1A==",
|
||||
"requires": {
|
||||
"@babel/core": "7.12.3",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "0.4.3",
|
||||
@@ -36257,8 +36252,8 @@
|
||||
},
|
||||
"water.css": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/water.css/download/water.css-2.1.1.tgz",
|
||||
"integrity": "sha1-7m/oM6MTo6LttYilftYrPjHe3Rg="
|
||||
"resolved": "https://registry.npmmirror.com/water.css/-/water.css-2.1.1.tgz",
|
||||
"integrity": "sha512-gkO5byC+pZ7ndEV18hs/RmxKoDtEZXx06tZU4ocI3IBdv4xV64tlhjIFbDjurysRnNkiy2oQTr8PakRyzZWPJw=="
|
||||
},
|
||||
"wbuf": {
|
||||
"version": "1.7.3",
|
||||
@@ -36270,8 +36265,8 @@
|
||||
},
|
||||
"web-vitals": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/web-vitals/download/web-vitals-1.1.2.tgz",
|
||||
"integrity": "sha1-BlNTCBaJhgliOaqEcW5otMaubRw="
|
||||
"resolved": "https://registry.npmmirror.com/web-vitals/-/web-vitals-1.1.2.tgz",
|
||||
"integrity": "sha512-PFMKIY+bRSXlMxVAQ+m2aw9c/ioUYfDgrYot0YUa+/xa0sakubWhSDyxAKwzymvXVdF4CZI71g06W+mqhzu6ig=="
|
||||
},
|
||||
"webidl-conversions": {
|
||||
"version": "6.1.0",
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-router": "^6.0.2",
|
||||
"react-router-dom": "^6.0.2",
|
||||
"react-scripts": "4.0.3",
|
||||
"react-router": "^6.3.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-scripts": "^4.0.3",
|
||||
"water.css": "^2.1.1",
|
||||
"web-vitals": "^1.1.2"
|
||||
},
|
||||
|
||||
@@ -17,8 +17,11 @@ body {
|
||||
}
|
||||
.title {
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: white;
|
||||
}
|
||||
.title-text {
|
||||
margin-left: 1em;
|
||||
@@ -92,3 +95,19 @@ dialog {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.horizontal {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.vertical {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.warp-word {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.number-input {
|
||||
width: 5em;
|
||||
}
|
||||
|
||||
103
web/src/App.js
103
web/src/App.js
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
HashRouter as Router,
|
||||
Routes,
|
||||
Route,
|
||||
NavLink,
|
||||
} from "react-router-dom";
|
||||
import { HashRouter as Router, Routes, Route, NavLink } from "react-router-dom";
|
||||
import "./App.css";
|
||||
|
||||
import GetRandomFiles from "./component/GetRandomFiles";
|
||||
@@ -11,32 +6,65 @@ import SearchFiles from "./component/SearchFiles";
|
||||
import SearchFolders from "./component/SearchFolders";
|
||||
import FilesInFolder from "./component/FilesInFolder";
|
||||
import Manage from "./component/Manage";
|
||||
import ManageUser from "./component/ManageUser";
|
||||
import FileInfo from "./component/FileInfo";
|
||||
import Share from "./component/Share";
|
||||
import Login from "./component/Login";
|
||||
import Register from "./component/Register";
|
||||
import Tags from "./component/Tags";
|
||||
import EditTag from "./component/EditTag";
|
||||
import EditReview from "./component/EditReview";
|
||||
import AudioPlayer from "./component/AudioPlayer";
|
||||
import { useState } from "react";
|
||||
import UserStatus from "./component/UserStatus";
|
||||
import ReviewPage from "./component/ReviewPage";
|
||||
import UserProfile from "./component/UserProfile";
|
||||
import FeedbackPage from "./component/FeedbackPage";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Tr, langCodeContext, LANG_OPTIONS } from "./translate";
|
||||
|
||||
function App() {
|
||||
const [playingFile, setPlayingFile] = useState({});
|
||||
const [user, setUser] = useState({});
|
||||
const [langCode, setLangCode] = useState("en_US");
|
||||
|
||||
// select language
|
||||
useEffect(() => {
|
||||
const browserCode = window.navigator.language;
|
||||
for (const key in LANG_OPTIONS) {
|
||||
for (const i in LANG_OPTIONS[key].matches) {
|
||||
const code = LANG_OPTIONS[key].matches[i];
|
||||
if (code === browserCode) {
|
||||
setLangCode(key);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// fallback to english
|
||||
setLangCode('en-US');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="base">
|
||||
<langCodeContext.Provider value={{ langCode, setLangCode }}>
|
||||
<Router>
|
||||
<header className="header">
|
||||
<h3 className="title">
|
||||
<img src="favicon.png" alt="logo" className="logo" />
|
||||
<span className="title-text">MSW Open Music Project</span>
|
||||
<UserStatus user={user} setUser={setUser} />
|
||||
</h3>
|
||||
<nav className="nav">
|
||||
<NavLink to="/" className="nav-link">
|
||||
Feeling luckly
|
||||
{Tr("Feeling luckly")}
|
||||
</NavLink>
|
||||
<NavLink to="/files" className="nav-link">
|
||||
Files
|
||||
{Tr("Files")}
|
||||
</NavLink>
|
||||
<NavLink to="/folders" className="nav-link">
|
||||
Folders
|
||||
{Tr("Folders")}
|
||||
</NavLink>
|
||||
<NavLink to="/manage" className="nav-link">
|
||||
Manage
|
||||
{Tr("Manage")}
|
||||
</NavLink>
|
||||
</nav>
|
||||
</header>
|
||||
@@ -59,20 +87,67 @@ function App() {
|
||||
path="/folders/:id"
|
||||
element={<FilesInFolder setPlayingFile={setPlayingFile} />}
|
||||
/>
|
||||
<Route path="/manage" element={<Manage />} />
|
||||
<Route
|
||||
path="/manage"
|
||||
element={
|
||||
<Manage
|
||||
user={user}
|
||||
setUser={setUser}
|
||||
setLangCode={setLangCode}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/manage/feedbacks"
|
||||
element={<FeedbackPage user={user} />}
|
||||
/>
|
||||
<Route
|
||||
path="/manage/login"
|
||||
element={<Login user={user} setUser={setUser} />}
|
||||
/>
|
||||
<Route
|
||||
path="/manage/register"
|
||||
element={<Register user={user} setUser={setUser} />}
|
||||
/>
|
||||
<Route path="/manage/tags" element={<Tags user={user} />} />
|
||||
<Route
|
||||
path="/manage/tags/:id"
|
||||
element={<EditTag user={user} />}
|
||||
/>
|
||||
<Route
|
||||
path="/manage/reviews/:id"
|
||||
element={<EditReview user={user} />}
|
||||
/>
|
||||
<Route
|
||||
path="/manage/users"
|
||||
element={<ManageUser user={user} setUser={setUser} />}
|
||||
/>
|
||||
<Route
|
||||
path="/manage/users/:id"
|
||||
element={<UserProfile user={user} setUser={setUser} />}
|
||||
/>
|
||||
<Route
|
||||
path="/files/:id"
|
||||
element={<FileInfo setPlayingFile={setPlayingFile} />}
|
||||
/>
|
||||
<Route
|
||||
path="/files/:id/share"
|
||||
element={<Share setPlayingFile={setPlayingFile} />}
|
||||
/>
|
||||
<Route
|
||||
path="/files/:id/review"
|
||||
element={
|
||||
<ReviewPage user={user} setPlayingFile={setPlayingFile} />
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</main>
|
||||
<footer>
|
||||
<AudioPlayer
|
||||
playingFile={playingFile}
|
||||
setPlayingFile={setPlayingFile}
|
||||
/>
|
||||
</footer>
|
||||
</Router>
|
||||
</langCodeContext.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useNavigate } from "react-router";
|
||||
import { CalcReadableFilesizeDetail } from "./Common";
|
||||
import FfmpegConfig from "./FfmpegConfig";
|
||||
import FileDialog from "./FileDialog";
|
||||
import { Tr } from "../translate";
|
||||
|
||||
function AudioPlayer(props) {
|
||||
// props.playingFile
|
||||
@@ -12,10 +13,14 @@ function AudioPlayer(props) {
|
||||
const [loop, setLoop] = useState(true);
|
||||
const [raw, setRaw] = useState(false);
|
||||
const [prepare, setPrepare] = useState(false);
|
||||
const [selectedFfmpegConfig, setSelectedFfmpegConfig] = useState({});
|
||||
const [selectedFfmpegConfig, setSelectedFfmpegConfig] = useState({
|
||||
name: "",
|
||||
args: "",
|
||||
});
|
||||
const [playingURL, setPlayingURL] = useState("");
|
||||
const [isPreparing, setIsPreparing] = useState(false);
|
||||
const [preparedFilesize, setPreparedFilesize] = useState(null);
|
||||
const [timerCount, setTimerCount] = useState(0);
|
||||
const [timerID, setTimerID] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
// no playing file
|
||||
@@ -40,7 +45,12 @@ function AudioPlayer(props) {
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
setPreparedFilesize(data.filesize);
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
setIsPreparing(false);
|
||||
return;
|
||||
}
|
||||
props.setPlayingFile(data.file);
|
||||
setIsPreparing(false);
|
||||
setPlayingURL(
|
||||
`/api/v1/get_file_stream_direct?id=${props.playingFile.id}&config=${selectedFfmpegConfig.name}`
|
||||
@@ -57,8 +67,8 @@ function AudioPlayer(props) {
|
||||
let navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h5>Player status</h5>
|
||||
<footer className="vertical">
|
||||
<h5>{Tr("Player status")}</h5>
|
||||
{props.playingFile.id && (
|
||||
<span>
|
||||
<FileDialog
|
||||
@@ -79,17 +89,13 @@ function AudioPlayer(props) {
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() =>
|
||||
navigate(`/folders/${props.playingFile.folder_id}`)
|
||||
}
|
||||
onClick={() => navigate(`/folders/${props.playingFile.folder_id}`)}
|
||||
>
|
||||
{props.playingFile.foldername}
|
||||
</button>
|
||||
|
||||
<button disabled>
|
||||
{prepare
|
||||
? CalcReadableFilesizeDetail(preparedFilesize)
|
||||
: CalcReadableFilesizeDetail(props.playingFile.filesize)}
|
||||
{CalcReadableFilesizeDetail(props.playingFile.filesize)}
|
||||
</button>
|
||||
|
||||
{isPreparing && <button disabled>Preparing...</button>}
|
||||
@@ -100,7 +106,7 @@ function AudioPlayer(props) {
|
||||
props.setPlayingFile({});
|
||||
}}
|
||||
>
|
||||
Stop
|
||||
{Tr("Stop")}
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
@@ -108,19 +114,53 @@ function AudioPlayer(props) {
|
||||
|
||||
<br />
|
||||
|
||||
<span className="horizontal">
|
||||
<input
|
||||
className="number-input"
|
||||
disabled={timerID !== null}
|
||||
type="number"
|
||||
value={timerCount}
|
||||
onChange={(e) => {
|
||||
setTimerCount(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (timerID != null) {
|
||||
clearInterval(timerID);
|
||||
setTimerID(null);
|
||||
return;
|
||||
}
|
||||
setTimerID(
|
||||
setTimeout(() => {
|
||||
props.setPlayingFile({});
|
||||
setTimerID(null);
|
||||
}, timerCount * 1000 * 60)
|
||||
);
|
||||
}}
|
||||
>
|
||||
{Tr("Stop Timer")}
|
||||
</button>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<span>
|
||||
<input
|
||||
checked={loop}
|
||||
onChange={(event) => setLoop(event.target.checked)}
|
||||
type="checkbox"
|
||||
/>
|
||||
<label>Loop</label>
|
||||
<label>{Tr("Loop")}</label>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<input
|
||||
checked={raw}
|
||||
onChange={(event) => setRaw(event.target.checked)}
|
||||
type="checkbox"
|
||||
/>
|
||||
<label>Raw</label>
|
||||
<label>{Tr("Raw")}</label>
|
||||
</span>
|
||||
|
||||
{!raw && (
|
||||
<span>
|
||||
@@ -129,12 +169,14 @@ function AudioPlayer(props) {
|
||||
onChange={(event) => setPrepare(event.target.checked)}
|
||||
type="checkbox"
|
||||
/>
|
||||
<label>Prepare</label>
|
||||
<label>{Tr("Prepare")}</label>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
{playingURL !== "" && (
|
||||
<audio
|
||||
id="dom-player"
|
||||
controls
|
||||
autoPlay
|
||||
loop={loop}
|
||||
@@ -147,7 +189,7 @@ function AudioPlayer(props) {
|
||||
selectedFfmpegConfig={selectedFfmpegConfig}
|
||||
setSelectedFfmpegConfig={setSelectedFfmpegConfig}
|
||||
/>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
import { useMemo } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
export function useQuery() {
|
||||
const { search } = useLocation();
|
||||
return useMemo(() => new URLSearchParams(search), [search]);
|
||||
}
|
||||
|
||||
export function CalcReadableFilesize(filesize) {
|
||||
if (filesize < 1024) {
|
||||
return filesize;
|
||||
@@ -35,6 +43,30 @@ function numberWithCommas(x) {
|
||||
return x;
|
||||
}
|
||||
|
||||
// convert unix timestamp to %Y-%m-%d %H:%M:%S
|
||||
export function convertIntToDateTime(timestamp) {
|
||||
var date = new Date(timestamp * 1000);
|
||||
var year = date.getFullYear();
|
||||
var month = date.getMonth() + 1;
|
||||
var day = date.getDate();
|
||||
var hour = date.getHours();
|
||||
var minute = date.getMinutes();
|
||||
var second = date.getSeconds();
|
||||
var time =
|
||||
year +
|
||||
"-" +
|
||||
(month < 10 ? "0" + month : month) +
|
||||
"-" +
|
||||
(day < 10 ? "0" + day : day) +
|
||||
" " +
|
||||
(hour < 10 ? "0" + hour : hour) +
|
||||
":" +
|
||||
(minute < 10 ? "0" + minute : minute) +
|
||||
":" +
|
||||
(second < 10 ? "0" + second : second);
|
||||
return time;
|
||||
}
|
||||
|
||||
export function SayHello() {
|
||||
return "Hello";
|
||||
}
|
||||
|
||||
114
web/src/component/Database.js
Normal file
114
web/src/component/Database.js
Normal file
@@ -0,0 +1,114 @@
|
||||
import { useState, useEffect, useContext } from "react";
|
||||
import { Tr, tr, langCodeContext } from "../translate";
|
||||
|
||||
function Database() {
|
||||
const [walkPath, setWalkPath] = useState("");
|
||||
const [patternString, setPatternString] = useState(
|
||||
"wav flac mp3 ogg m4a mka"
|
||||
);
|
||||
const [tags, setTags] = useState([]);
|
||||
const [selectedTags, setSelectedTags] = useState([]);
|
||||
const [updating, setUpdating] = useState(false);
|
||||
const { langCode } = useContext(langCodeContext);
|
||||
|
||||
function getTags() {
|
||||
fetch("/api/v1/get_tags")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
setTags(data.tags);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getTags();
|
||||
}, []);
|
||||
|
||||
function updateDatabase() {
|
||||
// split pattern string into array
|
||||
let patternArray = patternString.split(" ");
|
||||
// remove whitespace from array
|
||||
patternArray = patternArray.map((item) => item.trim());
|
||||
// remove empty strings from array
|
||||
patternArray = patternArray.filter((item) => item !== "");
|
||||
// add dot before item array
|
||||
patternArray = patternArray.map((item) => "." + item);
|
||||
|
||||
setUpdating(true);
|
||||
|
||||
fetch("/api/v1/walk", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
root: walkPath,
|
||||
pattern: patternArray,
|
||||
tag_ids: selectedTags,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
alert("Database updated");
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setUpdating(false);
|
||||
});
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h3>{Tr("Update Database")}</h3>
|
||||
<input
|
||||
type="text"
|
||||
value={walkPath}
|
||||
placeholder={tr("walk path", langCode)}
|
||||
onChange={(e) => setWalkPath(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={patternString}
|
||||
placeholder={tr("pattern wav flac mp3", langCode)}
|
||||
onChange={(e) => setPatternString(e.target.value)}
|
||||
/>
|
||||
<div>
|
||||
<h4>{Tr("Tags")}</h4>
|
||||
{tags.map((tag) => (
|
||||
<div key={tag.id}>
|
||||
<input
|
||||
id={tag.id}
|
||||
type="checkbox"
|
||||
value={tag.id}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedTags([...selectedTags, tag.id]);
|
||||
} else {
|
||||
setSelectedTags(
|
||||
selectedTags.filter((item) => item !== tag.id)
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<label htmlFor={tag.id}>{tag.name}</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
updateDatabase();
|
||||
}}
|
||||
disabled={updating}
|
||||
>
|
||||
{updating ? Tr("Updating...") : Tr("Update Database")}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Database;
|
||||
101
web/src/component/EditReview.js
Normal file
101
web/src/component/EditReview.js
Normal file
@@ -0,0 +1,101 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { useParams, useNavigate } from "react-router";
|
||||
import { tr, Tr, langCodeContext } from "../translate";
|
||||
|
||||
function SingleReview() {
|
||||
let params = useParams();
|
||||
let navigate = useNavigate();
|
||||
const { langCode } = useContext(langCodeContext)
|
||||
|
||||
const [review, setReview] = useState({
|
||||
id: "",
|
||||
user_id: "",
|
||||
file_id: "",
|
||||
content: "",
|
||||
created_at: "",
|
||||
updated_at: "",
|
||||
});
|
||||
|
||||
function refresh() {
|
||||
fetch("/api/v1/get_review", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
setReview(data.review);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function save() {
|
||||
fetch("/api/v1/update_review", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
content: review.content,
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
alert(tr("Review updated", langCode));
|
||||
navigate(-1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteReview() {
|
||||
fetch("/api/v1/delete_review", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
alert(tr("Review deleted", langCode));
|
||||
navigate(-1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>{Tr("Edit Review")}</h3>
|
||||
<textarea
|
||||
value={review.content}
|
||||
onChange={(e) => setReview({ ...review, content: e.target.value })}
|
||||
></textarea>
|
||||
<div>
|
||||
<button onClick={() => deleteReview()}>{Tr("Delete")}</button>
|
||||
<button onClick={() => save()}>{Tr("Save")}</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SingleReview;
|
||||
142
web/src/component/EditTag.js
Normal file
142
web/src/component/EditTag.js
Normal file
@@ -0,0 +1,142 @@
|
||||
import { useState, useEffect, useContext } from "react";
|
||||
import { useParams, useNavigate } from "react-router";
|
||||
import { tr, Tr, langCodeContext } from "../translate";
|
||||
|
||||
function EditTag() {
|
||||
let params = useParams();
|
||||
let navigate = useNavigate();
|
||||
const { langCode } = useContext(langCodeContext);
|
||||
|
||||
const [tag, setTag] = useState({
|
||||
id: "",
|
||||
name: "",
|
||||
description: "",
|
||||
created_by_user: {
|
||||
id: "",
|
||||
username: "",
|
||||
role: "",
|
||||
avatar_id: "",
|
||||
},
|
||||
});
|
||||
|
||||
function refreshTagInfo() {
|
||||
fetch("/api/v1/get_tag_info", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
setTag(data.tag);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateTagInfo() {
|
||||
fetch("/api/v1/update_tag", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
name: tag.name,
|
||||
description: tag.description,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
alert(tr("Tag updated successfully", langCode));
|
||||
refreshTagInfo();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refreshTagInfo();
|
||||
}, []);
|
||||
|
||||
function deleteTag() {
|
||||
fetch("/api/v1/delete_tag", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
alert(tr("Tag deleted successfully", langCode));
|
||||
navigate(-1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>{Tr("Edit Tag")}</h3>
|
||||
<div>
|
||||
<label htmlFor="id">{Tr("ID")}</label>
|
||||
<input
|
||||
type="text"
|
||||
disabled
|
||||
name="id"
|
||||
id="id"
|
||||
value={tag.id}
|
||||
onChange={(e) => setTag({ ...tag, id: e.target.value })}
|
||||
/>
|
||||
<label htmlFor="name">{Tr("Created by")}</label>
|
||||
<input
|
||||
type="text"
|
||||
disabled
|
||||
name="created_by_user_username"
|
||||
id="created_by_user_username"
|
||||
value={tag.created_by_user.username}
|
||||
onChange={(e) =>
|
||||
setTag({
|
||||
...tag,
|
||||
created_by_user: {
|
||||
...tag.created_by_user,
|
||||
username: e.target.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<label htmlFor="name">{Tr("Name")}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
value={tag.name}
|
||||
onChange={(e) => setTag({ ...tag, name: e.target.value })}
|
||||
/>
|
||||
<label htmlFor="description">{Tr("Description")}</label>
|
||||
<textarea
|
||||
name="description"
|
||||
id="description"
|
||||
value={tag.description}
|
||||
onChange={(e) => setTag({ ...tag, description: e.target.value })}
|
||||
/>
|
||||
<button onClick={deleteTag}>{Tr("Delete")}</button>
|
||||
<button onClick={() => updateTagInfo()}>{Tr("Save")}</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditTag;
|
||||
106
web/src/component/FeedbackPage.js
Normal file
106
web/src/component/FeedbackPage.js
Normal file
@@ -0,0 +1,106 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { convertIntToDateTime } from "./Common";
|
||||
import { Tr } from "../translate";
|
||||
|
||||
function FeedbackPage() {
|
||||
const [content, setContext] = useState("");
|
||||
const [feedbacks, setFeedbacks] = useState([]);
|
||||
|
||||
function getFeedbacks() {
|
||||
fetch("/api/v1/get_feedbacks")
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
console.log(data.error);
|
||||
} else {
|
||||
setFeedbacks(data.feedbacks);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function submitFeedback() {
|
||||
fetch("/api/v1/feedback", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: content,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
setContext("");
|
||||
getFeedbacks();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getFeedbacks();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>{Tr("Feedbacks")}</h3>
|
||||
<textarea value={content} onChange={(e) => setContext(e.target.value)} />
|
||||
<button onClick={() => submitFeedback()}>{Tr("Submit")}</button>
|
||||
<div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{Tr("User")}</th>
|
||||
<th>{Tr("Feedback")}</th>
|
||||
<th>{Tr("Date")}</th>
|
||||
<th>{Tr("Action")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{feedbacks.map((feedback) => (
|
||||
<tr key={feedback._id}>
|
||||
<td>
|
||||
<Link to={`/manage/users/${feedback.user.id}`}>
|
||||
@{feedback.user.username}
|
||||
</Link>
|
||||
</td>
|
||||
<td>{feedback.content}</td>
|
||||
<td>{convertIntToDateTime(feedback.time)}</td>
|
||||
<td>
|
||||
<button
|
||||
onClick={() => {
|
||||
fetch("/api/v1/delete_feedback", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: feedback.id,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
getFeedbacks();
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{Tr("Delete")}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FeedbackPage;
|
||||
@@ -33,7 +33,7 @@ function FfmpegConfig(props) {
|
||||
<option key={ffmpegConfig.name}>{ffmpegConfig.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<span>{props.selectedFfmpegConfig.args}</span>
|
||||
<span className="warp-word">{props.selectedFfmpegConfig.args}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useNavigate } from "react-router";
|
||||
import { Tr } from "../translate";
|
||||
|
||||
function FileDialog(props) {
|
||||
// props.showStatus
|
||||
@@ -9,37 +10,41 @@ function FileDialog(props) {
|
||||
|
||||
let navigate = useNavigate();
|
||||
|
||||
const downloadURL = "/api/v1/get_file_direct?id=" + props.file.id;
|
||||
|
||||
return (
|
||||
<dialog open={props.showStatus}>
|
||||
<p>{props.file.filename}</p>
|
||||
<p>
|
||||
Download 使用浏览器下载原文件
|
||||
<br />
|
||||
Play 调用网页播放器播放
|
||||
<br />
|
||||
<p
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
props.setPlayingFile(props.file);
|
||||
props.setShowStatus(false);
|
||||
}}
|
||||
>
|
||||
{props.file.filename}
|
||||
</p>
|
||||
<a href={downloadURL} download>
|
||||
<button>Download</button>
|
||||
</a>
|
||||
<p>
|
||||
{Tr("Play: play using browser player.")}
|
||||
<br />
|
||||
{Tr("Info for more actions.")}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate(`/files/${props.file.id}`);
|
||||
props.setShowStatus(false);
|
||||
}}
|
||||
>
|
||||
{Tr("Info")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
props.setPlayingFile(props.file);
|
||||
props.setShowStatus(false);
|
||||
}}
|
||||
>
|
||||
Play
|
||||
{Tr("Play")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate(`/files/${props.file.id}/share`);
|
||||
props.setShowStatus(false);
|
||||
}}
|
||||
>
|
||||
Share
|
||||
</button>
|
||||
<button onClick={() => props.setShowStatus(false)}>Close</button>
|
||||
<button onClick={() => props.setShowStatus(false)}>{Tr("Close")}</button>
|
||||
</dialog>
|
||||
);
|
||||
}
|
||||
|
||||
308
web/src/component/FileInfo.js
Normal file
308
web/src/component/FileInfo.js
Normal file
@@ -0,0 +1,308 @@
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { Tr, tr, langCodeContext } from "../translate";
|
||||
|
||||
function FileInfo(props) {
|
||||
let navigate = useNavigate();
|
||||
let params = useParams();
|
||||
const [file, setFile] = useState({
|
||||
id: "",
|
||||
folder_id: "",
|
||||
foldername: "",
|
||||
filename: "",
|
||||
filesize: "",
|
||||
});
|
||||
const [tags, setTags] = useState([]);
|
||||
const [tagsOnFile, setTagsOnFile] = useState([]);
|
||||
const [selectedTagID, setSelectedTagID] = useState("");
|
||||
const { langCode } = useContext(langCodeContext);
|
||||
|
||||
function refresh() {
|
||||
fetch(`/api/v1/get_file_info`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
setFile(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getTags() {
|
||||
fetch(`/api/v1/get_tags`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
setTags(data.tags);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getTagsOnFile() {
|
||||
fetch(`/api/v1/get_tags_on_file`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
setTagsOnFile(data.tags);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removeTagOnFile(tag_id) {
|
||||
fetch(`/api/v1/delete_tag_on_file`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
file_id: parseInt(params.id),
|
||||
tag_id: tag_id,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
getTagsOnFile();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteFile() {
|
||||
// show Warning
|
||||
if (
|
||||
window.confirm(tr("Are you sure you want to delete this file?", langCode))
|
||||
) {
|
||||
fetch(`/api/v1/delete_file`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
navigate(-1);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateFilename() {
|
||||
fetch(`/api/v1/update_filename`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
filename: file.filename,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
alert(tr("Filename updated", langCode));
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function resetFilename() {
|
||||
fetch(`/api/v1/reset_filename`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
getTags();
|
||||
getTagsOnFile();
|
||||
}, []);
|
||||
|
||||
const downloadURL = "/api/v1/get_file_direct?id=" + file.id;
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>{Tr("File Details")}</h3>
|
||||
<div>
|
||||
<a href={downloadURL} download>
|
||||
<button>{Tr("Download")}</button>
|
||||
</a>
|
||||
<button
|
||||
onClick={() => {
|
||||
props.setPlayingFile(file);
|
||||
}}
|
||||
>
|
||||
{Tr("Play")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate(`/files/${params.id}/review`);
|
||||
}}
|
||||
>
|
||||
{Tr("Review")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate(`/files/${params.id}/share`);
|
||||
}}
|
||||
>
|
||||
{Tr("Share")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
deleteFile();
|
||||
}}
|
||||
>
|
||||
{Tr("Delete")}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="foldername">{Tr("Folder Name")}</label>
|
||||
<input
|
||||
type="text"
|
||||
id="foldername"
|
||||
value={file.foldername}
|
||||
onClick={() => {
|
||||
navigate(`/folders/${file.folder_id}`);
|
||||
}}
|
||||
readOnly
|
||||
/>
|
||||
<label htmlFor="filename">{Tr("Filename")}</label>
|
||||
<input
|
||||
type="text"
|
||||
id="filename"
|
||||
value={file.filename}
|
||||
onChange={(event) => {
|
||||
setFile({
|
||||
...file,
|
||||
filename: event.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<label htmlFor="filesize">{Tr("File size")}</label>
|
||||
<input type="text" id="filesize" value={file.filesize} readOnly />
|
||||
</div>
|
||||
<div className="horizontal">
|
||||
<button onClick={updateFilename}>{Tr("Save")}</button>
|
||||
<button onClick={resetFilename}>{Tr("Reset")}</button>
|
||||
</div>
|
||||
<div>
|
||||
<label>{Tr("Tags")}</label>
|
||||
<ul>
|
||||
{tagsOnFile.map((tag) => {
|
||||
return (
|
||||
<li key={tag.id}>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate(`/manage/tags/${tag.id}`);
|
||||
}}
|
||||
>
|
||||
{tag.name}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
removeTagOnFile(tag.id);
|
||||
}}
|
||||
>
|
||||
{Tr("Remove")}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<div>
|
||||
<select
|
||||
onChange={(e) => {
|
||||
setSelectedTagID(e.target.value);
|
||||
}}
|
||||
>
|
||||
<option value="">{tr("Select a tag", langCode)}</option>
|
||||
{tags.map((tag) => {
|
||||
return (
|
||||
<option key={tag.id} value={tag.id}>
|
||||
{tag.name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
<button
|
||||
onClick={() => {
|
||||
// check empty
|
||||
if (selectedTagID === "") {
|
||||
alert(tr("Please select a tag", langCode));
|
||||
return;
|
||||
}
|
||||
fetch(`/api/v1/put_tag_on_file`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
file_id: parseInt(params.id),
|
||||
tag_id: parseInt(selectedTagID),
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
getTagsOnFile();
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{Tr("Add tag")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FileInfo;
|
||||
@@ -1,15 +1,21 @@
|
||||
import { useParams } from "react-router";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useQuery } from "./Common";
|
||||
import FilesTable from "./FilesTable";
|
||||
import { Tr } from "../translate";
|
||||
|
||||
function FilesInFolder(props) {
|
||||
let params = useParams();
|
||||
const query = useQuery();
|
||||
const navigator = useNavigate();
|
||||
const [files, setFiles] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [offset, setOffset] = useState(0);
|
||||
const offset = parseInt(query.get("o")) || 0;
|
||||
const [newFoldername, setNewFoldername] = useState("");
|
||||
const limit = 10;
|
||||
|
||||
useEffect(() => {
|
||||
function refresh() {
|
||||
setIsLoading(true);
|
||||
fetch("/api/v1/get_files_in_folder", {
|
||||
method: "POST",
|
||||
@@ -22,16 +28,27 @@ function FilesInFolder(props) {
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
setFiles(data.files ? data.files : []);
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
setFiles(data.files);
|
||||
if (data.files.length > 0) {
|
||||
setNewFoldername(data.files[0].foldername);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => alert(error))
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [params.id, offset]);
|
||||
|
||||
function nextPage() {
|
||||
setOffset(offset + limit);
|
||||
navigator(`/folders/${params.id}?o=${offset + limit}`);
|
||||
}
|
||||
|
||||
function lastPage() {
|
||||
@@ -39,20 +56,80 @@ function FilesInFolder(props) {
|
||||
if (offsetValue < 0) {
|
||||
return;
|
||||
}
|
||||
setOffset(offsetValue);
|
||||
navigator(`/folders/${params.id}?o=${offsetValue}`);
|
||||
}
|
||||
|
||||
function updateFoldername() {
|
||||
setIsLoading(true);
|
||||
fetch("/api/v1/update_foldername", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
foldername: newFoldername,
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
refresh();
|
||||
}
|
||||
})
|
||||
.catch((error) => alert(error))
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
function resetFoldername() {
|
||||
setIsLoading(true);
|
||||
fetch("/api/v1/reset_foldername", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
refresh();
|
||||
}
|
||||
})
|
||||
.catch((error) => alert(error))
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>Files in Folder</h3>
|
||||
<h3>{Tr("Files in Folder")}</h3>
|
||||
<div className="search_toolbar">
|
||||
<button onClick={lastPage}>Last page</button>
|
||||
<button onClick={lastPage}>{Tr("Last page")}</button>
|
||||
<button disabled>
|
||||
{isLoading ? "Loading..." : `${offset} - ${offset + files.length}`}
|
||||
{isLoading
|
||||
? Tr("Loading...")
|
||||
: `${offset} - ${offset + files.length}`}
|
||||
</button>
|
||||
<button onClick={nextPage}>Next page</button>
|
||||
<button onClick={nextPage}>{Tr("Next page")}</button>
|
||||
</div>
|
||||
<FilesTable setPlayingFile={props.setPlayingFile} files={files} />
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={newFoldername}
|
||||
onChange={(e) => setNewFoldername(e.target.value)}
|
||||
/>
|
||||
<div>
|
||||
<button onClick={() => updateFoldername()}>{Tr("Save")}</button>
|
||||
<button onClick={() => resetFoldername()}>{Tr("Reset")}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import FileEntry from "./FileEntry";
|
||||
import { Tr } from "../translate";
|
||||
|
||||
function FilesTable(props) {
|
||||
if (props.files.length === 0) {
|
||||
@@ -8,9 +9,9 @@ function FilesTable(props) {
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Filename</th>
|
||||
<th>Folder Name</th>
|
||||
<th>Size</th>
|
||||
<th>{Tr("Filename")}</th>
|
||||
<th>{Tr("Folder Name")}</th>
|
||||
<th>{Tr("Size")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useNavigate } from "react-router";
|
||||
import { Tr } from "../translate";
|
||||
|
||||
function FoldersTable(props) {
|
||||
let navigate = useNavigate();
|
||||
@@ -9,8 +10,8 @@ function FoldersTable(props) {
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Folder name</th>
|
||||
<th>Action</th>
|
||||
<th>{Tr("Folder name")}</th>
|
||||
<th>{Tr("Action")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -23,7 +24,7 @@ function FoldersTable(props) {
|
||||
{folder.foldername}
|
||||
</td>
|
||||
<td onClick={() => navigate(`/folders/${folder.id}`)}>
|
||||
<button>View</button>
|
||||
<button>{Tr("View")}</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useQuery } from "./Common";
|
||||
import FilesTable from "./FilesTable";
|
||||
import { Tr, tr, langCodeContext } from "../translate";
|
||||
|
||||
function GetRandomFiles(props) {
|
||||
const [files, setFiles] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [tags, setTags] = useState([]);
|
||||
const navigator = useNavigate();
|
||||
const query = useQuery();
|
||||
const selectedTag = query.get("t") || "";
|
||||
const { langCode } = useContext(langCodeContext);
|
||||
|
||||
function refresh(setFiles) {
|
||||
function getRandomFiles() {
|
||||
setIsLoading(true);
|
||||
fetch("/api/v1/get_random_files")
|
||||
.then((response) => response.json())
|
||||
@@ -20,15 +28,80 @@ function GetRandomFiles(props) {
|
||||
});
|
||||
}
|
||||
|
||||
function getRandomFilesWithTag() {
|
||||
setIsLoading(true);
|
||||
fetch("/api/v1/get_random_files_with_tag", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(selectedTag),
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
setFiles(data.files);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
alert("get_random_files_with_tag error: " + error);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
if (selectedTag === "") {
|
||||
getRandomFiles();
|
||||
} else {
|
||||
getRandomFilesWithTag();
|
||||
}
|
||||
}
|
||||
|
||||
function getTags() {
|
||||
fetch("/api/v1/get_tags")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
setTags(data.tags);
|
||||
})
|
||||
.catch((error) => {
|
||||
alert("get_tags error: " + error);
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refresh(setFiles);
|
||||
getTags();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [selectedTag]);
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<div className="search_toolbar">
|
||||
<button className="refresh" onClick={() => refresh(setFiles)}>
|
||||
{isLoading ? "Loading..." : "Refresh"}
|
||||
{isLoading ? Tr("Loading...") : Tr("Refresh")}
|
||||
</button>
|
||||
<select
|
||||
className="tag_select"
|
||||
onChange={(event) => {
|
||||
navigator(`/?t=${event.target.value}`);
|
||||
}}
|
||||
value={selectedTag}
|
||||
>
|
||||
<option value="">{tr("All", langCode)}</option>
|
||||
{tags.map((tag) => (
|
||||
<option key={tag.id} value={tag.id}>
|
||||
{tag.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<FilesTable setPlayingFile={props.setPlayingFile} files={files} />
|
||||
</div>
|
||||
|
||||
74
web/src/component/Login.js
Normal file
74
web/src/component/Login.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useContext, useState } from "react";
|
||||
import { Tr, tr, langCodeContext } from "../translate";
|
||||
|
||||
function Login(props) {
|
||||
let navigate = useNavigate();
|
||||
let [username, setUsername] = useState("");
|
||||
let [password, setPassword] = useState("");
|
||||
const { langCode } = useContext(langCodeContext);
|
||||
|
||||
function login() {
|
||||
if (!username || !password) {
|
||||
alert(tr("Please enter username and password", langCode));
|
||||
return;
|
||||
}
|
||||
fetch("/api/v1/login", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
password,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
return;
|
||||
}
|
||||
props.setUser(data.user);
|
||||
navigate("/");
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h2>{Tr("Login")}</h2>
|
||||
<label htmlFor="username">{Tr("Username")}</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
<label htmlFor="password">{Tr("Password")}</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
login();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span>
|
||||
<button onClick={login}>{Tr("Login")}</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate("/manage/register");
|
||||
}}
|
||||
>
|
||||
{Tr("Register")}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Login;
|
||||
@@ -1,49 +1,85 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import Database from "./Database";
|
||||
|
||||
function Manage() {
|
||||
const [token, setToken] = useState("");
|
||||
const [walkPath, setWalkPath] = useState("");
|
||||
import { Tr, langCodeContext, LANG_OPTIONS } from "../translate";
|
||||
import { useContext } from "react";
|
||||
|
||||
function updateDatabase() {
|
||||
fetch("/api/v1/walk", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
token: token,
|
||||
root: walkPath,
|
||||
pattern: [".wav", ".mp3"],
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
console.log(data);
|
||||
});
|
||||
}
|
||||
function Manage(props) {
|
||||
let navigate = useNavigate();
|
||||
const { langCode, setLangCode } = useContext(langCodeContext);
|
||||
const codes = Object.keys(LANG_OPTIONS);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Manage</h2>
|
||||
<input
|
||||
type="text"
|
||||
value={token}
|
||||
placeholder="token"
|
||||
onChange={(e) => setToken(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={walkPath}
|
||||
placeholder="walk path"
|
||||
onChange={(e) => setWalkPath(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
updateDatabase();
|
||||
<div className="page">
|
||||
<h2>{Tr("Manage")}</h2>
|
||||
<p>
|
||||
{Tr("Hi")}, {props.user.username}
|
||||
</p>
|
||||
|
||||
<select
|
||||
onChange={(event) => {
|
||||
setLangCode(codes[event.target.selectedIndex]);
|
||||
}}
|
||||
>
|
||||
Update Database
|
||||
{codes.map((code) => {
|
||||
const langOption = LANG_OPTIONS[code];
|
||||
return <option key={code}>{langOption.name}</option>;
|
||||
})}
|
||||
</select>
|
||||
|
||||
{props.user.role === 0 && (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate("/manage/login");
|
||||
}}
|
||||
>
|
||||
{Tr("Login")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate("/manage/register");
|
||||
}}
|
||||
>
|
||||
{Tr("Register")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{props.user.role !== 0 && (
|
||||
<div className="horizontal">
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate(`/manage/users/${props.user.id}`);
|
||||
}}
|
||||
>
|
||||
{Tr("Profile")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
fetch("/api/v1/logout")
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
props.setUser(data.user);
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{Tr("Logout")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<hr />
|
||||
<div className="horizontal">
|
||||
<button onClick={() => navigate("/manage/tags")}>{Tr("Tags")}</button>
|
||||
<button onClick={() => navigate("/manage/users")}>{Tr("Users")}</button>
|
||||
<button onClick={() => navigate("/manage/feedbacks")}>
|
||||
{Tr("Feedbacks")}
|
||||
</button>
|
||||
</div>
|
||||
<Database />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
81
web/src/component/ManageUser.js
Normal file
81
web/src/component/ManageUser.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Tr } from "../translate";
|
||||
|
||||
function ManageUser() {
|
||||
const [users, setUsers] = useState([]);
|
||||
const roleDict = {
|
||||
0: "Anonymous",
|
||||
1: "Admin",
|
||||
2: "User",
|
||||
};
|
||||
|
||||
function getUsers() {
|
||||
fetch("/api/v1/get_users")
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
setUsers(data.users);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getUsers();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>{Tr("Manage User")}</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{Tr("Name")}</th>
|
||||
<th>{Tr("Role")}</th>
|
||||
<th>{Tr("Active")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map((user) => (
|
||||
<tr key={user.id}>
|
||||
<td>
|
||||
<Link to={`/manage/users/${user.id}`}>@{user.username}</Link>
|
||||
</td>
|
||||
<td>{Tr(roleDict[user.role])}</td>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
defaultChecked={user.active}
|
||||
onClick={(e) => {
|
||||
fetch("/api/v1/update_user_active", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: user.id,
|
||||
active: e.target.checked,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
getUsers();
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ManageUser;
|
||||
82
web/src/component/Register.js
Normal file
82
web/src/component/Register.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useContext, useState } from "react";
|
||||
import { tr, Tr, langCodeContext } from "../translate";
|
||||
|
||||
function Register() {
|
||||
let navigate = useNavigate();
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [password2, setPassword2] = useState("");
|
||||
const [role, setRole] = useState("");
|
||||
const { langCode } = useContext(langCodeContext);
|
||||
|
||||
function register() {
|
||||
if (!username || !password || !password2 || !role) {
|
||||
alert(tr("Please fill out all fields", langCode));
|
||||
} else if (password !== password2) {
|
||||
alert(tr("Password do not match", langCode));
|
||||
} else {
|
||||
fetch("/api/v1/register", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: username,
|
||||
password: password,
|
||||
role: parseInt(role),
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
navigate("/manage/login");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h2>{Tr("Register")}</h2>
|
||||
<label htmlFor="username">{Tr("Username")}</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
<label htmlFor="password">{Tr("Password")}</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<label htmlFor="password2">{Tr("Confirm Password")}</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password2"
|
||||
value={password2}
|
||||
onChange={(e) => setPassword2(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
register();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<label htmlFor="role">{Tr("Role")}</label>
|
||||
<select value={role} onChange={(e) => setRole(e.target.value)}>
|
||||
<option value="">{tr("Select a role", langCode)}</option>
|
||||
<option value="2">{tr("User", langCode)}</option>
|
||||
<option value="1">{tr("Admin", langCode)}</option>
|
||||
</select>
|
||||
<button onClick={register}>{Tr("Register")}</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Register;
|
||||
33
web/src/component/ReviewEntry.js
Normal file
33
web/src/component/ReviewEntry.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { convertIntToDateTime } from "./Common";
|
||||
import { Tr, tr, langCodeContext } from "../translate";
|
||||
import { useContext } from "react";
|
||||
|
||||
function ReviewEntry(props) {
|
||||
const { langCode } = useContext(langCodeContext);
|
||||
return (
|
||||
<div>
|
||||
<h4>
|
||||
<Link to={`/manage/users/${props.review.user.id}`}>
|
||||
@{props.review.user.username}
|
||||
</Link>{" "}
|
||||
{Tr("review")}{" "}
|
||||
<Link to={`/files/${props.review.file.id}`}>
|
||||
{props.review.file.filename}
|
||||
</Link>{" "}
|
||||
{Tr("on")} {convertIntToDateTime(props.review.created_at)}{" "}
|
||||
{props.review.updated_at !== 0 &&
|
||||
`(${tr("modified on", langCode)} ${convertIntToDateTime(
|
||||
props.review.updated_at
|
||||
)} ) `}
|
||||
{(props.user.role === 1 || props.review.user.id === props.user.id) &&
|
||||
props.user.role !== 0 && (
|
||||
<Link to={`/manage/reviews/${props.review.id}`}>{Tr("Edit")}</Link>
|
||||
)}
|
||||
</h4>
|
||||
<p>{props.review.content}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReviewEntry;
|
||||
76
web/src/component/ReviewPage.js
Normal file
76
web/src/component/ReviewPage.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useParams } from "react-router";
|
||||
import ReviewEntry from "./ReviewEntry";
|
||||
import { Tr } from "../translate";
|
||||
|
||||
function ReviewPage(props) {
|
||||
let params = useParams();
|
||||
const [newReview, setNewReview] = useState("");
|
||||
const [reviews, setReviews] = useState([]);
|
||||
|
||||
function refresh() {
|
||||
fetch("/api/v1/get_reviews_on_file", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
setReviews(data.reviews);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, []);
|
||||
|
||||
function submitReview() {
|
||||
fetch("/api/v1/insert_review", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: newReview,
|
||||
file_id: parseInt(params.id),
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
setNewReview("");
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>{Tr("Review Page")}</h3>
|
||||
<div>
|
||||
{reviews.map((review) => (
|
||||
<ReviewEntry key={review.id} review={review} user={props.user} />
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<textarea
|
||||
value={newReview}
|
||||
onChange={(e) => setNewReview(e.target.value)}
|
||||
/>
|
||||
<button onClick={() => submitReview()}>{Tr("Submit")}</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReviewPage;
|
||||
@@ -1,14 +1,25 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useContext } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useQuery } from "./Common";
|
||||
import FilesTable from "./FilesTable";
|
||||
import { Tr, tr, langCodeContext } from "../translate";
|
||||
|
||||
function SearchFiles(props) {
|
||||
const navigator = useNavigate();
|
||||
const [files, setFiles] = useState([]);
|
||||
const [filename, setFilename] = useState("");
|
||||
const [offset, setOffset] = useState(0);
|
||||
const query = useQuery();
|
||||
const filename = query.get("q") || "";
|
||||
const [filenameInput, setFilenameInput] = useState(filename);
|
||||
const offset = parseInt(query.get("o")) || 0;
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const limit = 10;
|
||||
const { langCode } = useContext(langCodeContext);
|
||||
|
||||
function searchFiles() {
|
||||
// check empty filename
|
||||
if (filename === "") {
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
fetch("/api/v1/search_files", {
|
||||
method: "POST",
|
||||
@@ -33,7 +44,7 @@ function SearchFiles(props) {
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
setOffset(offset + limit);
|
||||
navigator(`/files?q=${filenameInput}&o=${offset + limit}`);
|
||||
}
|
||||
|
||||
function lastPage() {
|
||||
@@ -41,37 +52,38 @@ function SearchFiles(props) {
|
||||
if (offsetValue < 0) {
|
||||
return;
|
||||
}
|
||||
setOffset(offsetValue);
|
||||
navigator(`/files?q=${filenameInput}&o=${offsetValue}`);
|
||||
}
|
||||
|
||||
useEffect(() => searchFiles(), [offset]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
useEffect(() => searchFiles(), [offset, filename]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>Search Files</h3>
|
||||
<h3>{Tr("Search Files")}</h3>
|
||||
<div className="search_toolbar">
|
||||
<input
|
||||
onChange={(event) => setFilename(event.target.value)}
|
||||
onChange={(event) => setFilenameInput(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
searchFiles();
|
||||
navigator(`/files?q=${filenameInput}&o=0`);
|
||||
}
|
||||
}}
|
||||
type="text"
|
||||
placeholder="Enter filename"
|
||||
placeholder={tr("Enter filename", langCode)}
|
||||
value={filenameInput}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
searchFiles();
|
||||
navigator(`/files?q=${filenameInput}&o=0`);
|
||||
}}
|
||||
>
|
||||
{isLoading ? "Loading..." : "Search"}
|
||||
{isLoading ? Tr("Loading...") : Tr("Search")}
|
||||
</button>
|
||||
<button onClick={lastPage}>Last page</button>
|
||||
<button onClick={lastPage}>{Tr("Last page")}</button>
|
||||
<button disabled>
|
||||
{offset} - {offset + files.length}
|
||||
</button>
|
||||
<button onClick={nextPage}>Next page</button>
|
||||
<button onClick={nextPage}>{Tr("Next page")}</button>
|
||||
</div>
|
||||
<FilesTable setPlayingFile={props.setPlayingFile} files={files} />
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useQuery } from "./Common";
|
||||
import FoldersTable from "./FoldersTable";
|
||||
import { Tr, tr, langCodeContext } from "../translate";
|
||||
|
||||
function SearchFolders() {
|
||||
const [foldername, setFoldername] = useState("");
|
||||
const navigator = useNavigate();
|
||||
const query = useQuery();
|
||||
const foldername = query.get("q") || "";
|
||||
const [foldernameInput, setFoldernameInput] = useState(foldername);
|
||||
const [folders, setFolders] = useState([]);
|
||||
const [offset, setOffset] = useState(0);
|
||||
const offset = parseInt(query.get("o")) || 0;
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const limit = 10;
|
||||
const { langCode } = useContext(langCodeContext);
|
||||
|
||||
function searchFolder() {
|
||||
if (foldername === "") {
|
||||
@@ -35,7 +42,7 @@ function SearchFolders() {
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
setOffset(offset + limit);
|
||||
navigator(`/folders?q=${foldername}&o=${offset + limit}`);
|
||||
}
|
||||
|
||||
function lastPage() {
|
||||
@@ -43,33 +50,38 @@ function SearchFolders() {
|
||||
if (offsetValue < 0) {
|
||||
return;
|
||||
}
|
||||
setOffset(offsetValue);
|
||||
navigator(`/folders?q=${foldername}&o=${offsetValue}`);
|
||||
}
|
||||
|
||||
useEffect(() => searchFolder(), [offset]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
useEffect(() => searchFolder(), [offset, foldername]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>Search Folders</h3>
|
||||
<h3>{Tr("Search Folders")}</h3>
|
||||
<div className="search_toolbar">
|
||||
<input
|
||||
onChange={(event) => setFoldername(event.target.value)}
|
||||
onChange={(event) => setFoldernameInput(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
searchFolder();
|
||||
navigator(`/folders?q=${foldernameInput}&o=0`);
|
||||
}
|
||||
}}
|
||||
type="text"
|
||||
placeholder="Enter folder name"
|
||||
placeholder={tr("Enter folder name", langCode)}
|
||||
value={foldernameInput}
|
||||
/>
|
||||
<button onClick={searchFolder}>
|
||||
{isLoading ? "Loading..." : "Search"}
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator(`/folders?q=${foldernameInput}&o=0`);
|
||||
}}
|
||||
>
|
||||
{isLoading ? Tr("Loading...") : Tr("Search")}
|
||||
</button>
|
||||
<button onClick={lastPage}>Last page</button>
|
||||
<button onClick={lastPage}>{Tr("Last page")}</button>
|
||||
<button disabled>
|
||||
{offset} - {offset + limit}
|
||||
</button>
|
||||
<button onClick={nextPage}>Next page</button>
|
||||
<button onClick={nextPage}>{Tr("Next page")}</button>
|
||||
</div>
|
||||
<FoldersTable folders={folders} />
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router";
|
||||
import FilesTable from "./FilesTable";
|
||||
import { Tr } from "../translate";
|
||||
|
||||
function Share(props) {
|
||||
let params = useParams();
|
||||
@@ -23,13 +24,14 @@ function Share(props) {
|
||||
}, [params]);
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>Share with others!</h3>
|
||||
<h3>{Tr("Share with others!")}</h3>
|
||||
<p>
|
||||
👇 Click the filename below to enjoy music!
|
||||
<br />
|
||||
{Tr("Share link")}:{" "}
|
||||
<a href={window.location.href}>{window.location.href}</a>
|
||||
</p>
|
||||
<p>
|
||||
Share link: <a href={window.location.href}>{window.location.href}</a>
|
||||
👇 {Tr("Click the filename below to enjoy music!")}
|
||||
<br />
|
||||
</p>
|
||||
<FilesTable setPlayingFile={props.setPlayingFile} files={file} />
|
||||
</div>
|
||||
|
||||
106
web/src/component/Tags.js
Normal file
106
web/src/component/Tags.js
Normal file
@@ -0,0 +1,106 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Tr } from "../translate";
|
||||
|
||||
function Tags() {
|
||||
const [tags, setTags] = useState([]);
|
||||
const [newTagName, setNewTagName] = useState("");
|
||||
const [newTagDescription, setNewTagDescription] = useState("");
|
||||
const [showAddTag, setShowAddTag] = useState(false);
|
||||
|
||||
function refresh() {
|
||||
fetch("/api/v1/get_tags")
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
setTags(data.tags);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>{Tr("Tags")}</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{Tr("Name")}</th>
|
||||
<th>{Tr("Description")}</th>
|
||||
<th>{Tr("Created by")}</th>
|
||||
<th>{Tr("Action")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tags.map((tag) => (
|
||||
<tr key={tag.id}>
|
||||
<td>{tag.name}</td>
|
||||
<td>{tag.description}</td>
|
||||
<td>
|
||||
<Link to={`/manage/users/${tag.created_by_user.id}`}>
|
||||
@{tag.created_by_user.username}
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
<Link to={`/manage/tags/${tag.id}`}>{Tr("Edit")}</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{!showAddTag && (
|
||||
<button onClick={() => setShowAddTag(true)}>{Tr("Add tag")}</button>
|
||||
)}
|
||||
{showAddTag && (
|
||||
<div>
|
||||
<label htmlFor="newTagName">{Tr("New Tag Name")}</label>
|
||||
<input
|
||||
type="text"
|
||||
id="newTagName"
|
||||
value={newTagName}
|
||||
onChange={(e) => setNewTagName(e.target.value)}
|
||||
/>
|
||||
<label htmlFor="newTagDescription">{Tr("New Tag Description")}</label>
|
||||
<textarea
|
||||
id="newTagDescription"
|
||||
value={newTagDescription}
|
||||
onChange={(e) => setNewTagDescription(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
fetch("/api/v1/insert_tag", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: newTagName,
|
||||
description: newTagDescription,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
setNewTagName("");
|
||||
setNewTagDescription("");
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{Tr("Create tag")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Tags;
|
||||
170
web/src/component/UserProfile.js
Normal file
170
web/src/component/UserProfile.js
Normal file
@@ -0,0 +1,170 @@
|
||||
import { useState, useEffect, useContext } from "react";
|
||||
import { useParams } from "react-router";
|
||||
import ReviewEntry from "./ReviewEntry";
|
||||
import { Tr, tr, langCodeContext } from "../translate";
|
||||
|
||||
function UserProfile(props) {
|
||||
let params = useParams();
|
||||
const [reviews, setReviews] = useState([]);
|
||||
const [oldPassword, setOldPassword] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [newPasswordConfirm, setNewPasswordConfirm] = useState("");
|
||||
const [user, setUser] = useState({
|
||||
id: 0,
|
||||
username: "",
|
||||
role: 0,
|
||||
active: false,
|
||||
avatar_id: 0,
|
||||
});
|
||||
const { langCode } = useContext(langCodeContext);
|
||||
|
||||
function getReviews() {
|
||||
fetch("/api/v1/get_reviews_by_user", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
setReviews(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getUserInfo() {
|
||||
fetch("/api/v1/get_user_info", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
setUser(data.user);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getReviews();
|
||||
getUserInfo();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>{Tr("User Profile")}</h3>
|
||||
<div className="horizontal">
|
||||
<input
|
||||
type="text"
|
||||
value={user.username}
|
||||
onChange={(e) => {
|
||||
setUser({
|
||||
...user,
|
||||
username: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
fetch("/api/v1/update_username", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
username: user.username,
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
props.setUser({
|
||||
...props.user,
|
||||
username: user.username,
|
||||
});
|
||||
alert("Username updated successfully!");
|
||||
getUserInfo();
|
||||
}
|
||||
});
|
||||
}}
|
||||
disabled={props.user.id !== user.id && props.user.role !== 1}
|
||||
>
|
||||
{Tr("Save username")}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="password"
|
||||
value={oldPassword}
|
||||
placeholder={tr("Old password", langCode)}
|
||||
onChange={(e) => setOldPassword(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
placeholder={tr("New password", langCode)}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
value={newPasswordConfirm}
|
||||
placeholder={tr("Confirm new password", langCode)}
|
||||
onChange={(e) => setNewPasswordConfirm(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
fetch("/api/v1/update_user_password", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
old_password: oldPassword,
|
||||
new_password: newPassword,
|
||||
new_password_confirm: newPasswordConfirm,
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
alert(tr("Password updated successfully!", langCode));
|
||||
}
|
||||
});
|
||||
}}
|
||||
disabled={
|
||||
(props.user.id !== user.id && props.user.role !== 1) ||
|
||||
newPassword !== newPasswordConfirm ||
|
||||
newPassword.length === 0
|
||||
}
|
||||
>
|
||||
{Tr("Change password")}
|
||||
</button>
|
||||
</div>
|
||||
<h4>{Tr("Reviews")}</h4>
|
||||
{reviews.map((review) => (
|
||||
<ReviewEntry key={review.id} review={review} user={props.user} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserProfile;
|
||||
16
web/src/component/UserStatus.js
Normal file
16
web/src/component/UserStatus.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
function UserStatus(props) {
|
||||
// props.user
|
||||
// props.setUser
|
||||
useEffect(() => {
|
||||
fetch("/api/v1/login")
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
props.setUser(data.user);
|
||||
});
|
||||
}, []);
|
||||
return <div>{props.user.username}</div>;
|
||||
}
|
||||
|
||||
export default UserStatus;
|
||||
44
web/src/translate/index.js
Normal file
44
web/src/translate/index.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { createContext, renderToString } from "react";
|
||||
import MAP_zh_CN from "./zh_CN";
|
||||
|
||||
const LANG_OPTIONS = {
|
||||
"en-US": {
|
||||
name: "English",
|
||||
langMap: {},
|
||||
matches: ["en-US", "en"],
|
||||
},
|
||||
"zh-CN": {
|
||||
name: "中文(简体)",
|
||||
langMap: MAP_zh_CN,
|
||||
matches: ["zh-CN", "zh"],
|
||||
},
|
||||
};
|
||||
|
||||
const langCodeContext = createContext("en-US");
|
||||
|
||||
function tr(text, langCode) {
|
||||
const option = LANG_OPTIONS[langCode];
|
||||
if (option === undefined) {
|
||||
return text;
|
||||
}
|
||||
const langMap = LANG_OPTIONS[langCode].langMap;
|
||||
|
||||
const translatedText = langMap[text.toLowerCase()];
|
||||
if (translatedText === undefined) {
|
||||
return text;
|
||||
}
|
||||
|
||||
return translatedText;
|
||||
}
|
||||
|
||||
function Tr(text) {
|
||||
return (
|
||||
<langCodeContext.Consumer>
|
||||
{({ langCode }) => {
|
||||
return tr(text, langCode);
|
||||
}}
|
||||
</langCodeContext.Consumer>
|
||||
);
|
||||
}
|
||||
|
||||
export { tr, Tr, LANG_OPTIONS, langCodeContext };
|
||||
105
web/src/translate/zh_CN.js
Normal file
105
web/src/translate/zh_CN.js
Normal file
@@ -0,0 +1,105 @@
|
||||
const LANG_zh_CN = {
|
||||
"feeling luckly": "随机",
|
||||
files: "文件",
|
||||
folders: "文件夹",
|
||||
manage: "管理",
|
||||
"manage user": "用户管理",
|
||||
active: "激活",
|
||||
"search files": "搜索文件",
|
||||
"search folders": "搜索文件夹",
|
||||
"enter filename": "输入文件名",
|
||||
"enter folder name": "输入文件名",
|
||||
search: "搜索",
|
||||
"last page": "上一页",
|
||||
all: "全部",
|
||||
"loading...": "加载中...",
|
||||
"next page": "下一页",
|
||||
"search polders": "搜索文件夹",
|
||||
"share with others!": "分享给好友!",
|
||||
"click the filename below to enjoy music!": "点击下面的文件名开始享受音乐!",
|
||||
"share link": "分享链接",
|
||||
hi: "您好",
|
||||
profile: "个人信息",
|
||||
"user profile": "用户信息",
|
||||
"save username": "更改用户名",
|
||||
save: "保存",
|
||||
reset: "重置",
|
||||
"old password": "旧密码",
|
||||
"new password": "新密码",
|
||||
"confirm new password": "确认新密码",
|
||||
"change password": "更改密码",
|
||||
reviews: "评论",
|
||||
review: "评论",
|
||||
on: "在",
|
||||
edit: "编辑",
|
||||
"modified on": "修改于",
|
||||
share: "分享",
|
||||
delete: "删除",
|
||||
remove: "移除",
|
||||
"file details": "文件详情",
|
||||
download: "下载",
|
||||
logout: "登出",
|
||||
tags: "标签",
|
||||
"add tag": "添加标签",
|
||||
"select a tag": "选择一个标签",
|
||||
"review page": "评论页面",
|
||||
submit: "提交",
|
||||
users: "用户",
|
||||
feedbacks: "反馈",
|
||||
feedback: "反馈",
|
||||
date: "时间",
|
||||
action: "操作",
|
||||
"new tag name": "新标签名",
|
||||
"new tag description": "新标签描述",
|
||||
"update database": "更新索引",
|
||||
"updating...": "更新中...",
|
||||
refresh: "刷新",
|
||||
filename: "文件名",
|
||||
"folder name": "文件夹名",
|
||||
size: "大小",
|
||||
"player status": "播放状态",
|
||||
play: "播放",
|
||||
stop: "停止",
|
||||
"stop timer": "定时停止",
|
||||
loop: "循环",
|
||||
raw: "无损",
|
||||
prepare: "预转码",
|
||||
"file size": "文件大小",
|
||||
login: "登陆",
|
||||
register: "注册",
|
||||
"play: play using browser player.": "播放: 使用浏览器播放",
|
||||
"info for more actions.": "详细: 查看更多相关信息",
|
||||
info: "详细",
|
||||
close: "关闭",
|
||||
"please enter username and password": "请输入用户名和密码",
|
||||
username: "用户名",
|
||||
password: "密码",
|
||||
"please fill out all fields": "请完整填写所有信息",
|
||||
"password do not match": "两次密码不一致",
|
||||
"password updated successfully!": "密码已成功更新!",
|
||||
role: "身份",
|
||||
user: "用户",
|
||||
admin: "管理员",
|
||||
anonymous: "匿名",
|
||||
"select a role": "选择身份",
|
||||
"walk path": "遍历目录",
|
||||
"pattern wav flac mp3": "拓展名 wav flac mp3",
|
||||
"review updated": "已修改评论",
|
||||
"review deleted": "已删除评论",
|
||||
"edit review": "编辑评论",
|
||||
view: "查看",
|
||||
"tag updated successfully": "标签修改成功",
|
||||
"tag deleted successfully": "标签删除成功",
|
||||
"edit tag": "编辑标签",
|
||||
id: "编号",
|
||||
"created by": "创建者",
|
||||
"create tag": "创建新标签",
|
||||
name: "名称",
|
||||
description: "描述",
|
||||
"are you sure you want to delete this file?": "你确定要删除这个文件吗?",
|
||||
"filename updated": "已修改文件名",
|
||||
"please select a tag": "请选择一个标签",
|
||||
"files in folder": "文件夹内",
|
||||
};
|
||||
|
||||
export default LANG_zh_CN;
|
||||
Reference in New Issue
Block a user