Compare commits
160 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
6cff4247a8
|
|||
|
90ff1382a8
|
|||
|
1450357b91
|
|||
|
7a31c36c10
|
|||
|
5271c12525
|
|||
|
d278e4009d
|
|||
|
e3d80ffc2a
|
|||
|
30a8fbad4e
|
|||
|
e7bc625b6d
|
|||
|
2d71e7b0cb
|
|||
|
2297f87fa3
|
|||
|
271c7e5c13
|
|||
|
9fdea6b169
|
|||
|
38e20044e2
|
|||
|
830cbae17a
|
|||
|
51fee5bfe0
|
|||
|
e40fd2625f
|
|||
|
061ef9bdc9
|
|||
|
d478923ce0
|
|||
|
e4032069a5
|
|||
|
73da4f8dc5
|
|||
|
89ff2bf452
|
|||
|
d0f6d19a7e
|
|||
|
977b3e02e9
|
|||
|
a6d82c1f47
|
|||
|
b808d4be99
|
|||
|
5c3fb66db3
|
|||
|
df081d39ca
|
|||
|
2f2254371b
|
|||
|
857a5e9dd9
|
|||
|
2b4bbdf25e
|
|||
|
08a5650b30
|
|||
|
8a9569ea61
|
|||
|
dc380590e7
|
|||
|
e5fa4c2b65
|
|||
|
97693d6bd0
|
|||
|
edd5eeb4c0
|
|||
|
adf0c24f91
|
|||
|
539fbb6501
|
|||
|
f5dec2a0a7
|
|||
|
da59740b47
|
|||
|
cae07f55cd
|
|||
|
84cf09e61b
|
|||
|
36c1990e5e
|
|||
|
21e51756f0
|
|||
|
caf8b47ca0
|
|||
|
51e5f2d0fb
|
|||
|
b0280767cb
|
|||
|
85f25a38ae
|
|||
|
00399785d4
|
|||
|
f3e69b032f
|
|||
|
824866bdd8
|
|||
|
0b76ea08cd
|
|||
|
ba1e96db26
|
|||
|
cb5c752f8f
|
|||
|
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
|
18
.drone.yml
Normal file
18
.drone.yml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: default
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: frontend-web
|
||||||
|
image: node:19
|
||||||
|
commands:
|
||||||
|
- cd web
|
||||||
|
- npm install
|
||||||
|
- npm run build
|
||||||
|
|
||||||
|
- name: release
|
||||||
|
image: plugins/gitea-release
|
||||||
|
settings:
|
||||||
|
api_key: da966507c259aa32ccc2d434e930af4a580de785
|
||||||
|
base_url: https://yongyuancv.cn/git/
|
||||||
|
files: dist/*
|
||||||
68
.github/workflows/build.yml
vendored
Normal file
68
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# This is a basic workflow to help you get started with Actions
|
||||||
|
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
# Controls when the workflow will run
|
||||||
|
on:
|
||||||
|
# Triggers the workflow on push or pull request events but only for the "master" branch
|
||||||
|
push:
|
||||||
|
branches: [ "master" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "master" ]
|
||||||
|
|
||||||
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||||
|
jobs:
|
||||||
|
build-backend-linux:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: 1.18
|
||||||
|
- name: Build linux backend
|
||||||
|
run: |
|
||||||
|
make linux
|
||||||
|
- name: Upload linux backend
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: backend-linux
|
||||||
|
path: |
|
||||||
|
msw-open-music
|
||||||
|
config.json
|
||||||
|
build-backend-windows:
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: 1.18
|
||||||
|
- name: Build windows backend
|
||||||
|
run: |
|
||||||
|
go build -v
|
||||||
|
- name: Upload linux backend
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: backend-windows
|
||||||
|
path: |
|
||||||
|
msw-open-music.exe
|
||||||
|
config.json
|
||||||
|
build-frontend-web:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
CI: false
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
- name: Build web front end
|
||||||
|
run: |
|
||||||
|
make web
|
||||||
|
- name: upload packaged front end
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: frontend-web
|
||||||
|
path: web/build
|
||||||
7
Makefile
7
Makefile
@@ -1,9 +1,10 @@
|
|||||||
dist:
|
.PHONY: web linux windows
|
||||||
|
web:
|
||||||
cd web && npm install
|
cd web && npm install
|
||||||
cd web && npm run build
|
cd web && npm run build
|
||||||
|
|
||||||
linux:
|
linux:
|
||||||
go build
|
go build -v -ldflags '-linkmode=external -extldflags=-static' -tags sqlite_omit_load_extension,netgo
|
||||||
|
|
||||||
windows:
|
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
|
||||||
|
|||||||
137
README-cn.md
Normal file
137
README-cn.md
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
# MSW Open Music Project
|
||||||
|
|
||||||
|
[](https://github.com/heimoshuiyu/msw-open-music/actions/workflows/build.yml)
|
||||||
|
|
||||||
|
🔴 演示 Demo: <https://msw-open-music.live>
|
||||||
|
|
||||||
|
> 找一首歌最好的方法是:打开一个超长的歌单,然后随机播放,直到你找到为止。
|
||||||
|
|
||||||
|
一个 💪 轻量级 ⚡️ 高性能 🖥️ 跨平台的 个人音乐串流平台。管理你现有的音乐文件并在其他设备上播放。
|
||||||
|
|
||||||
|
前端网页应用基于 `react.js` 和 `water.css` 构建。后端服务器程序使用 `golang` 和 `sqlite` 构建。
|
||||||
|
|
||||||
|
## 介绍
|
||||||
|
|
||||||
|
截图
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 功能特点
|
||||||
|
|
||||||
|
- 🔎 索引现有的音乐文件,并记录文件名和文件夹元信息
|
||||||
|
|
||||||
|
- 📕 使用 文件夹 📁 标签 🏷️ 评论 💬 来管理你的音乐。
|
||||||
|
|
||||||
|
- 🌐 提供一个轻量高效的网页前端并支持多种语言。
|
||||||
|
|
||||||
|
- 👥 支持多用户。
|
||||||
|
|
||||||
|
- 🔥 调用 `ffmpeg` 配合可自定义的预设配置来转码你的音乐。
|
||||||
|
|
||||||
|
- 🔗 分享音乐链接给好友!
|
||||||
|
|
||||||
|
### 如果你遇到过这样的烦恼...你就是目标用户
|
||||||
|
|
||||||
|
- 硬盘上存了一堆音乐,但没有一个很好的播放器. 🖴
|
||||||
|
|
||||||
|
- 下载了体积非常大的无损音乐,在设备间移动很困难. 🎵
|
||||||
|
|
||||||
|
- 想要在其他 电脑/手机 上听 电脑/服务器 上储存的音乐. 😋
|
||||||
|
|
||||||
|
- 想给你的好友分享本地音乐. 😘
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
1. 修改 `config.json` 配置文件中的 `secret` 值
|
||||||
|
|
||||||
|
2. 运行后端服务器程序 `msw-open-music.exe` 或者 `msw-open-music`. 服务默认监听 8080 端口。 然后打开 <http://127.0.0.1:8080> 去创建的一个管理员帐号。
|
||||||
|
|
||||||
|
前端 HTML 文件存放在 `web/build` 目录下。
|
||||||
|
|
||||||
|
### 创建第一个管理员帐号
|
||||||
|
|
||||||
|
第一个创建的管理员帐号会被自动激活,其他后续创建的管理员帐号需要管理员手动激活。
|
||||||
|
|
||||||
|
请前往注册页面,选择角色为 管理员,然后注册第一个管理员帐号。
|
||||||
|
|
||||||
|
#### config.json
|
||||||
|
|
||||||
|
- `secret` 字符串类型。用来加密 session 会话。
|
||||||
|
|
||||||
|
- `database_name` 字符串类型。`sqlite3` 数据库的文件名。如果不存在,会自动创建。
|
||||||
|
- `addr` 字符串类型。监听地址和端口。
|
||||||
|
- `ffmpeg_config_list` 列表类型。预设的 `ffmpeg` 配置文件。包含 `ffmpegConfig` 对象。
|
||||||
|
- `file_life_time` 整数类型(秒)。临时文件的生命周期。如果临时文件超过这个时间没有被访问,那么将会被自动删除。
|
||||||
|
- `cleaner_internal` 整数类型(秒)。`tmpfs` 检查临时文件的间隔时间。
|
||||||
|
- `root` 字符串类型。存放临时文件的目录。默认是 `/tmp`。**Windows用户请修改成可用的目录**。如果不存在,将会被自动创建。
|
||||||
|
- `permission` 各个 API 的权限等级。
|
||||||
|
- `0` 无需任何权限。
|
||||||
|
- `1` 需要管理员(最高级别)权限等级。
|
||||||
|
- `2` 需要普通用户权限等级,也就是说,管理员和普通用户都有权访问此等级的 API ,而 匿名用户 则没有权限访问。
|
||||||
|
- 如果你想避免 API 被滥用,可以调整下面 5 个与串流相关的 API 权限等级。
|
||||||
|
- `/get_file` 使用 `io.copy()` 方法串流
|
||||||
|
- `/get_file_direct` 使用 `http.serveFile()` 方法串流
|
||||||
|
- `/get_file_stream` 调用 `ffmpeg` 并串流其标准输出 `stdout`
|
||||||
|
- `/prepare_file_stream_direct` 调用 `ffmpeg` 预转码一个文件
|
||||||
|
- `/get_file_stream_direct` 使用 `http.serveFile()` 获取预转码结束的临时文件
|
||||||
|
- 其他在 `config.json` 中没有设定的 API 将默认拥有 `0` 的权限等级。
|
||||||
|
|
||||||
|
对于 Windows 用户,请确保 `ffmpeg` 正确安装并设置环境变量。
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
欢迎任何 issue / pull request / feature request
|
||||||
|
|
||||||
|
### 主要变更历史
|
||||||
|
|
||||||
|
- `v1.0.0` 第一个版本。核心串流功能可用。
|
||||||
|
- `v1.1.0` 使用 `React` 重构前端。
|
||||||
|
- `v1.2.0` 数据库 DBMS 课程作业。添加 用户、标签、评论 和其他功能。
|
||||||
|
|
||||||
|
### ER Diagram
|
||||||
|
|
||||||
|
Database Entities Relationship Diagram
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- `avatar` 目前没有在使用。
|
||||||
|
|
||||||
|
- 第一次运行程序时,程序会自动创建一个 ID 为 `1` 的匿名用户。所有未登陆的用户都会自动登陆到这个账户。
|
||||||
|
|
||||||
|
- `tmpfs` 储存在内存中,每次重新启动后端程序将会清空记录的信息。
|
||||||
|
|
||||||
|
### 关于 tmpfs
|
||||||
|
|
||||||
|
如果前端的播放器勾选了 `预转码` 选项,后端程序会先将文件转码到临时目录中,转码完成后再串流文件。这么做可以实现断点续传,解决由于网络波动导致 `ffmpeg` 管道链接断开而终止转码的问题。
|
||||||
|
|
||||||
|
默认的临时文件夹目录是 `/tmp`,这是 Linux 系统中通用的临时目录。默认的生存时间是 600 秒(10 分钟)。如果超过这个时间没有访问该临时文件,那么后端程序将会自动删除它。
|
||||||
|
|
||||||
|
### 后端 API 设计
|
||||||
|
|
||||||
|
一个不需要返回任何有用数据的 API 将会返回下面的 JSON 对象
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "OK"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
当错误发生时,后端会返回如下格式的 JSON 对象。`error` 是对错误信息的详细描述文本。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Wrong password"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
不需要传递参数的 API 使用 `GET` 方法,否则使用 `POST` 方法。(忽略 RESTFUL 设计)
|
||||||
|
|
||||||
|
后端使用 cookies 来实现用户会话管理。任何不带 cookies 的请求会被认为是由 匿名用户 发送的(也就是 ID 为 `1` 的用户)
|
||||||
|
|
||||||
|
一些重要的源代码文件
|
||||||
|
|
||||||
|
- `pkg/api/api.go` 定义各个 API 的 URL 和对应函数。
|
||||||
|
|
||||||
|
- `pkg/database/sql_stmt.go` 定义 SQL 语句和做一些初始化工作。
|
||||||
|
|
||||||
|
- `pkg/database/struct.go` 定义 JSON 和 数据库对象 的 数据结构。
|
||||||
406
README.md
406
README.md
@@ -1,373 +1,139 @@
|
|||||||
# MSW Open Music Project
|
# MSW Open Music Project
|
||||||
|
|
||||||
|
[](https://github.com/heimoshuiyu/msw-open-music/actions/workflows/build.yml)
|
||||||
|
|
||||||
|
🔴 Demo: <https://msw-open-music.live>
|
||||||
|
|
||||||
|
[中文文档](./README-cn.md)
|
||||||
|
|
||||||
|
> 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
|
## 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
|
## 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.
|
### Setup first admin account
|
||||||
- Others URL matched `/*` will be served files under `web/build/`
|
|
||||||
|
|
||||||
### 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.
|
- `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.
|
- `addr` string type. The listen address and port.
|
||||||
- `token` string type. Password.
|
|
||||||
- `ffmpeg_config_list` list type, include `ffmpegConfig` object.
|
- `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.
|
- `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.
|
- `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.
|
||||||
|
- `permission`. Specify each API's permission level.
|
||||||
|
- `0` for no permission required.
|
||||||
|
- `1` require admin level (highest level) permission.
|
||||||
|
- `2` require normal user level permission. That is, both admins and registered users can access to this URL, except anonymous users.
|
||||||
|
- If you want to avoid abuse of the playback API, you can adjust the permission level for these 5 playback-related APIs.
|
||||||
|
- `/get_file` get file with `io.copy()` method
|
||||||
|
- `/get_file_direct` get file with `http.serveFile()` method
|
||||||
|
- `/get_file_stream` call ffmpeg and stream its `stdout` output
|
||||||
|
- `/prepare_file_stream_direct` call ffmpeg to convert a file
|
||||||
|
- `/get_file_stream_direct` get the converted file with `http.serveFile()`
|
||||||
|
- Other URLs not metion in `config.json` will have `0` permission level by default.
|
||||||
|
|
||||||
### 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.
|
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
|
### Back-end API design
|
||||||
|
|
||||||
- `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.
|
|
||||||
|
|
||||||
API does not need to respond any data will return the following JSON object.
|
API does not need to respond any data will return the following JSON object.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"status": "OK"
|
"status": "OK"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 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
|
- `pkg/api/api.go` define URL
|
||||||
{
|
|
||||||
"id": 123
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- `/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
|
- `pkg/database/struct.go` define JSON structures for database entities.
|
||||||
|
|
||||||
`/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.
|
|
||||||
|
|||||||
99
config.json
99
config.json
@@ -1,23 +1,80 @@
|
|||||||
{
|
{
|
||||||
"api": {
|
"api": {
|
||||||
"database_name": "music.sqlite3",
|
"secret": "CHANGE_YOUR_SECRET_HERE",
|
||||||
"addr": ":8080",
|
"database_name": "postgres://postgres:woshimima@localhost/postgres?sslmode=disable",
|
||||||
"token": "!! config your very strong token here !!",
|
"single_thread": true,
|
||||||
"ffmpeg_threads": 1,
|
"addr": ":8080",
|
||||||
"ffmpeg_config_list": [
|
"ffmpeg_threads": 1,
|
||||||
{"name": "OPUS 128k", "args": "-c:a libopus -ab 128k"},
|
"ffmpeg_config_list": [
|
||||||
{"name": "OPUS 96k", "args": "-c:a libopus -ab 96k"},
|
{
|
||||||
{"name": "OPUS 256k", "args": "-c:a libopus -ab 256k"},
|
"name": "WEBM OPUS 128k",
|
||||||
{"name": "OPUS 320k", "args": "-c:a libopus -ab 320k"},
|
"args": "-c:a libopus -ab 128k -vn",
|
||||||
{"name": "OPUS 512k", "args": "-c:a libopus -ab 512k"},
|
"format": "webm"
|
||||||
{"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 96k",
|
||||||
]
|
"args": "-c:a libopus -ab 96k -vn",
|
||||||
},
|
"format": "webm"
|
||||||
"tmpfs": {
|
},
|
||||||
"file_life_time": 600,
|
{
|
||||||
"cleaner_internal": 1,
|
"name": "WEBM OPUS 256k",
|
||||||
"root": "/tmp/"
|
"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"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"permission": {
|
||||||
|
"/register": 0,
|
||||||
|
"/get_file": 0,
|
||||||
|
"/get_file_direct": 0,
|
||||||
|
"/get_file_stream": 0,
|
||||||
|
"/prepare_file_stream_direct": 0,
|
||||||
|
"/get_file_stream_direct": 0,
|
||||||
|
"/walk": 1,
|
||||||
|
"/reset": 1,
|
||||||
|
"/update_user_active": 1,
|
||||||
|
"/get_feedbacks": 1,
|
||||||
|
"/delete_feedback": 1,
|
||||||
|
"/delete_file": 1,
|
||||||
|
"/update_filename": 1,
|
||||||
|
"/reset_filename": 1,
|
||||||
|
"/reset_foldername": 1,
|
||||||
|
"/update_foldername": 1,
|
||||||
|
"/insert_tag": 1,
|
||||||
|
"/update_tag": 1,
|
||||||
|
"/delete_tag": 1,
|
||||||
|
"/put_tag_on_file": 1,
|
||||||
|
"/delete_tag_on_file": 1,
|
||||||
|
"/delete_review": 2,
|
||||||
|
"/update_review": 2,
|
||||||
|
"/update_user_password": 2,
|
||||||
|
"/update_username": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tmpfs": {
|
||||||
|
"file_life_time": 600,
|
||||||
|
"cleaner_internal": 1,
|
||||||
|
"root": "/tmp/"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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
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
|
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/lib/pq v1.10.7
|
||||||
|
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/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||||
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
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/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
|
||||||
|
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
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"
|
"flag"
|
||||||
"log"
|
"log"
|
||||||
"msw-open-music/pkg/api"
|
"msw-open-music/pkg/api"
|
||||||
|
"msw-open-music/pkg/commonconfig"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,12 +15,11 @@ func init() {
|
|||||||
flag.StringVar(&ConfigFilePath, "config", "config.json", "backend config file path")
|
flag.StringVar(&ConfigFilePath, "config", "config.json", "backend config file path")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var err error
|
var err error
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
config := api.Config{}
|
config := commonconfig.Config{}
|
||||||
configFile, err := os.Open(ConfigFilePath)
|
configFile, err := os.Open(ConfigFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
@@ -37,7 +37,6 @@ func main() {
|
|||||||
log.Println("Starting",
|
log.Println("Starting",
|
||||||
config.APIConfig.DatabaseName,
|
config.APIConfig.DatabaseName,
|
||||||
config.APIConfig.Addr,
|
config.APIConfig.Addr,
|
||||||
config.APIConfig.Token,
|
|
||||||
)
|
)
|
||||||
log.Fatal(api.Server.ListenAndServe())
|
log.Fatal(api.Server.ListenAndServe())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,35 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
"msw-open-music/pkg/commonconfig"
|
||||||
"msw-open-music/pkg/database"
|
"msw-open-music/pkg/database"
|
||||||
"msw-open-music/pkg/tmpfs"
|
"msw-open-music/pkg/tmpfs"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
type API struct {
|
type API struct {
|
||||||
Db *database.Database
|
Db *database.Database
|
||||||
Server http.Server
|
Server http.Server
|
||||||
token string
|
APIConfig commonconfig.APIConfig
|
||||||
APIConfig APIConfig
|
Tmpfs *tmpfs.Tmpfs
|
||||||
Tmpfs *tmpfs.Tmpfs
|
store *sessions.CookieStore
|
||||||
|
defaultSessionName string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAPIConfig() APIConfig {
|
func NewAPI(config commonconfig.Config) (*API, error) {
|
||||||
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) {
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
apiConfig := config.APIConfig
|
apiConfig := config.APIConfig
|
||||||
tmpfsConfig := config.TmpfsConfig
|
tmpfsConfig := config.TmpfsConfig
|
||||||
|
|
||||||
db, err := database.NewDatabase(apiConfig.DatabaseName)
|
db, err := database.NewDatabase(apiConfig.DatabaseName, apiConfig.SingleThread)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
store := sessions.NewCookieStore([]byte(config.APIConfig.SECRET))
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
apiMux := http.NewServeMux()
|
apiMux := http.NewServeMux()
|
||||||
|
|
||||||
@@ -52,7 +39,9 @@ func NewAPI(config Config) (*API, error) {
|
|||||||
Addr: apiConfig.Addr,
|
Addr: apiConfig.Addr,
|
||||||
Handler: mux,
|
Handler: mux,
|
||||||
},
|
},
|
||||||
APIConfig: apiConfig,
|
APIConfig: apiConfig,
|
||||||
|
store: store,
|
||||||
|
defaultSessionName: "msw-open-music",
|
||||||
}
|
}
|
||||||
api.Tmpfs = tmpfs.NewTmpfs(tmpfsConfig)
|
api.Tmpfs = tmpfs.NewTmpfs(tmpfsConfig)
|
||||||
|
|
||||||
@@ -64,21 +53,57 @@ func NewAPI(config Config) (*API, error) {
|
|||||||
apiMux.HandleFunc("/search_folders", api.HandleSearchFolders)
|
apiMux.HandleFunc("/search_folders", api.HandleSearchFolders)
|
||||||
apiMux.HandleFunc("/get_files_in_folder", api.HandleGetFilesInFolder)
|
apiMux.HandleFunc("/get_files_in_folder", api.HandleGetFilesInFolder)
|
||||||
apiMux.HandleFunc("/get_random_files", api.HandleGetRandomFiles)
|
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_file_stream", api.HandleGetFileStream)
|
||||||
apiMux.HandleFunc("/get_ffmpeg_config_list", api.HandleGetFfmpegConfigs)
|
apiMux.HandleFunc("/get_ffmpeg_config_list", api.HandleGetFfmpegConfigs)
|
||||||
apiMux.HandleFunc("/feedback", api.HandleFeedback)
|
|
||||||
apiMux.HandleFunc("/get_file_info", api.HandleGetFileInfo)
|
apiMux.HandleFunc("/get_file_info", api.HandleGetFileInfo)
|
||||||
|
apiMux.HandleFunc("/get_file_ffprobe_info", api.HandleGetFileFfprobeInfo)
|
||||||
apiMux.HandleFunc("/get_file_stream_direct", api.HandleGetFileStreamDirect)
|
apiMux.HandleFunc("/get_file_stream_direct", api.HandleGetFileStreamDirect)
|
||||||
|
apiMux.HandleFunc("/get_file_avatar", api.HandelGetFileAvatar)
|
||||||
apiMux.HandleFunc("/prepare_file_stream_direct", api.HandlePrepareFileStreamDirect)
|
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.HandleLoginAsAnonymous)
|
||||||
|
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)
|
||||||
|
// statistic
|
||||||
|
apiMux.HandleFunc("/record_playback", api.HandleRecordPlayback)
|
||||||
|
// database
|
||||||
apiMux.HandleFunc("/walk", api.HandleWalk)
|
apiMux.HandleFunc("/walk", api.HandleWalk)
|
||||||
apiMux.HandleFunc("/reset", api.HandleReset)
|
apiMux.HandleFunc("/reset", api.HandleReset)
|
||||||
apiMux.HandleFunc("/add_ffmpeg_config", api.HandleAddFfmpegConfig)
|
|
||||||
|
|
||||||
mux.Handle("/api/v1/", http.StripPrefix("/api/v1", apiMux))
|
mux.Handle("/api/v1/", http.StripPrefix("/api/v1", api.PermissionMiddleware(apiMux)))
|
||||||
mux.Handle("/", http.StripPrefix("/", http.FileServer(http.Dir("web/build"))))
|
mux.Handle("/", http.StripPrefix("/", http.FileServer(http.Dir("web/build"))))
|
||||||
|
|
||||||
api.token = apiConfig.Token
|
|
||||||
|
|
||||||
return api, nil
|
return api, nil
|
||||||
}
|
}
|
||||||
|
|||||||
108
pkg/api/handle_avatar.go
Normal file
108
pkg/api/handle_avatar.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"msw-open-music/pkg/database"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (api *API) HandelGetFileAvatar(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var err error
|
||||||
|
q := r.URL.Query()
|
||||||
|
ids := q["id"]
|
||||||
|
if len(ids) == 0 {
|
||||||
|
err = errors.New(`parameter "id" can't be empty`)
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := strconv.Atoi(ids[0])
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
file, err := api.Db.GetFile(int64(id))
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
path, err := file.Path()
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Println("[api] Get avatar of file", path)
|
||||||
|
buff := make([]byte, 0)
|
||||||
|
cache := bytes.NewBuffer(buff)
|
||||||
|
cmd := exec.Command("ffmpeg", "-i", path, "-c:v", "libwebp_anim", "-update", "1", "-frames:v", "1", "-f", "image2pipe", "-")
|
||||||
|
cmd.Stdout = cache
|
||||||
|
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
api.HandleGetAlternativeFileAvatar(w, r, file)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "image/webp")
|
||||||
|
io.Copy(w, cache)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) HandleGetAlternativeFileAvatar(w http.ResponseWriter, r *http.Request, f *database.File) {
|
||||||
|
var err error
|
||||||
|
dir, err := f.Dir()
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Println("[api] Get alternative avatar in dir", dir)
|
||||||
|
files, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
avatar, err := findAvatarFile(files)
|
||||||
|
avatarPath := path.Join(dir, avatar)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cmd := exec.Command("ffmpeg", "-i", avatarPath, "-c:v", "libwebp_anim", "-f", "image2pipe", "-")
|
||||||
|
cmd.Stdout = w
|
||||||
|
w.Header().Set("Content-Type", "image/webp")
|
||||||
|
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func findAvatarFile(files []os.DirEntry) (string, error) {
|
||||||
|
for _, file := range files {
|
||||||
|
if isAvatarType(file.Name()) {
|
||||||
|
return file.Name(), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", errors.New("Cannot find avatar file")
|
||||||
|
}
|
||||||
|
|
||||||
|
var avatarFileTypes = []string{
|
||||||
|
".jpg",
|
||||||
|
".png",
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAvatarType(filename string) bool {
|
||||||
|
for _, t := range avatarFileTypes {
|
||||||
|
if strings.HasSuffix(strings.ToLower(filename), t) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -2,32 +2,20 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WalkRequest struct {
|
type WalkRequest struct {
|
||||||
Token string `json:"token"`
|
|
||||||
Root string `json:"root"`
|
Root string `json:"root"`
|
||||||
Pattern []string `json:"pattern"`
|
Pattern []string `json:"pattern"`
|
||||||
}
|
TagIDs []int64 `json:"tag_ids"`
|
||||||
|
|
||||||
type ResetRequest struct {
|
|
||||||
Token string `json:"token"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) HandleReset(w http.ResponseWriter, r *http.Request) {
|
func (api *API) HandleReset(w http.ResponseWriter, r *http.Request) {
|
||||||
resetRequest := &ResetRequest{}
|
var err error
|
||||||
err := json.NewDecoder(r.Body).Decode(resetRequest)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// check token
|
log.Println("[api] Reset database")
|
||||||
err = api.CheckToken(w, r, resetRequest.Token)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// reset
|
// reset
|
||||||
err = api.Db.ResetFiles()
|
err = api.Db.ResetFiles()
|
||||||
@@ -52,12 +40,6 @@ func (api *API) HandleWalk(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// check token match
|
|
||||||
err = api.CheckToken(w, r, walkRequest.Token)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// check root empty
|
// check root empty
|
||||||
if walkRequest.Root == "" {
|
if walkRequest.Root == "" {
|
||||||
api.HandleErrorString(w, r, `key "root" can't be empty`)
|
api.HandleErrorString(w, r, `key "root" can't be empty`)
|
||||||
@@ -70,8 +52,17 @@ func (api *API) HandleWalk(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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
|
// walk
|
||||||
err = api.Db.Walk(walkRequest.Root, walkRequest.Pattern)
|
err = api.Db.Walk(walkRequest.Root, walkRequest.Pattern, walkRequest.TagIDs, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.HandleError(w, r, err)
|
api.HandleError(w, r, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -2,10 +2,24 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"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) {
|
func (api *API) HandleError(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
api.HandleErrorString(w, r, 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) {
|
func (api *API) HandleErrorStringCode(w http.ResponseWriter, r *http.Request, errorString string, code int) {
|
||||||
log.Println("[api] [Error]", code, errorString)
|
log.Println("[api] [Error]", code, errorString)
|
||||||
errStatus := &Status{
|
errStatus := &Error{
|
||||||
Status: errorString,
|
Error: errorString,
|
||||||
}
|
}
|
||||||
w.WriteHeader(code)
|
w.WriteHeader(code)
|
||||||
json.NewEncoder(w).Encode(errStatus)
|
json.NewEncoder(w).Encode(errStatus)
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log"
|
"log"
|
||||||
|
"msw-open-music/pkg/database"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FeedbackRequest struct {
|
type FeedbackRequest struct {
|
||||||
Feedback string `json:"feedback"`
|
Content string `json:"content"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) HandleFeedback(w http.ResponseWriter, r *http.Request) {
|
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
|
// check empty feedback
|
||||||
if feedbackRequest.Feedback == "" {
|
if feedbackRequest.Content == "" {
|
||||||
api.HandleErrorString(w, r, `"feedback" can't be empty`)
|
api.HandleErrorString(w, r, `"feedback" can't be empty`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("[api] Feedback", feedbackRequest.Feedback)
|
log.Println("[api] Feedback", feedbackRequest.Content)
|
||||||
|
|
||||||
headerBuff := &bytes.Buffer{}
|
headerBuff := &bytes.Buffer{}
|
||||||
err = r.Header.Write(headerBuff)
|
err = r.Header.Write(headerBuff)
|
||||||
@@ -36,10 +37,59 @@ func (api *API) HandleFeedback(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
header := headerBuff.String()
|
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 {
|
if err != nil {
|
||||||
api.HandleError(w, r, err)
|
api.HandleError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
api.HandleOK(w, r)
|
api.HandleOK(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GetFeedbacksResponse struct {
|
||||||
|
Feedbacks []*database.Feedback `json:"feedbacks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) HandleGetFeedbacks(w http.ResponseWriter, r *http.Request) {
|
||||||
|
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) {
|
||||||
|
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 (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"log"
|
"log"
|
||||||
|
"msw-open-music/pkg/commonconfig"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FfmpegConfig struct {
|
func (api *API) GetFfmpegConfig(configName string) (commonconfig.FfmpegConfig, bool) {
|
||||||
Name string `json:"name"`
|
ffmpegConfig := commonconfig.FfmpegConfig{}
|
||||||
Args string `json:"args"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FfmpegConfigList struct {
|
|
||||||
FfmpegConfigList []FfmpegConfig `json:"ffmpeg_config_list"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) GetFfmpegConfig(configName string) (FfmpegConfig, bool) {
|
|
||||||
ffmpegConfig := FfmpegConfig{}
|
|
||||||
for _, f := range api.APIConfig.FfmpegConfigList {
|
for _, f := range api.APIConfig.FfmpegConfigList {
|
||||||
if f.Name == configName {
|
if f.Name == configName {
|
||||||
ffmpegConfig = f
|
ffmpegConfig = f
|
||||||
@@ -30,45 +22,8 @@ func (api *API) GetFfmpegConfig(configName string) (FfmpegConfig, bool) {
|
|||||||
|
|
||||||
func (api *API) HandleGetFfmpegConfigs(w http.ResponseWriter, r *http.Request) {
|
func (api *API) HandleGetFfmpegConfigs(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Println("[api] Get ffmpeg config list")
|
log.Println("[api] Get ffmpeg config list")
|
||||||
ffmpegConfigList := &FfmpegConfigList{
|
ffmpegConfigList := &commonconfig.FfmpegConfigList{
|
||||||
FfmpegConfigList: api.APIConfig.FfmpegConfigList,
|
FfmpegConfigList: api.APIConfig.FfmpegConfigList,
|
||||||
}
|
}
|
||||||
json.NewEncoder(w).Encode(&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,7 +5,9 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,6 +32,8 @@ func (api *API) HandleGetFileInfo(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Println("[api] Get file info", getFileRequest.ID)
|
||||||
|
|
||||||
file, err := api.Db.GetFile(getFileRequest.ID)
|
file, err := api.Db.GetFile(getFileRequest.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.HandleError(w, r, err)
|
api.HandleError(w, r, err)
|
||||||
@@ -43,10 +47,8 @@ func (api *API) HandleGetFileInfo(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// /get_file
|
func (api *API) HandleGetFileFfprobeInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
// get raw file with io.Copy method
|
getFileRequest := &GetFileRequest {
|
||||||
func (api *API) HandleGetFile(w http.ResponseWriter, r *http.Request) {
|
|
||||||
getFileRequest := &GetFileRequest{
|
|
||||||
ID: -1,
|
ID: -1,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,12 +64,52 @@ func (api *API) HandleGetFile(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Println("[api] Get file Ffprobe info", getFileRequest.ID)
|
||||||
|
|
||||||
file, err := api.Db.GetFile(getFileRequest.ID)
|
file, err := api.Db.GetFile(getFileRequest.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.HandleError(w, r, err)
|
api.HandleError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
path, err := file.Path()
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cmd := exec.Command("ffprobe", "-i", path, "-hide_banner")
|
||||||
|
cmd.Stderr = w
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// /api/v1/get_file?id=123
|
||||||
|
// get raw file with io.Copy method
|
||||||
|
func (api *API) HandleGetFile(w http.ResponseWriter, r *http.Request) {
|
||||||
|
q := r.URL.Query()
|
||||||
|
ids := q["id"]
|
||||||
|
_id, err := strconv.Atoi(ids[0])
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id := int64(_id)
|
||||||
|
|
||||||
|
// check empty
|
||||||
|
if id < 0 {
|
||||||
|
api.HandleErrorString(w, r, `"id" can't be none or negative`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := api.Db.GetFile(id)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
path, err := file.Path()
|
path, err := file.Path()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.HandleError(w, r, err)
|
api.HandleError(w, r, err)
|
||||||
@@ -111,6 +153,12 @@ func (api *API) HandleGetFileDirect(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
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)
|
log.Println("[api] Get direct raw file", path)
|
||||||
|
|
||||||
http.ServeFile(w, r, path)
|
http.ServeFile(w, r, path)
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ type GetFilesInFolderRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GetFilesInFolderResponse struct {
|
type GetFilesInFolderResponse struct {
|
||||||
Files *[]database.File `json:"files"`
|
Files *[]database.File `json:"files"`
|
||||||
|
Folder string `json:"folder"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) HandleGetFilesInFolder(w http.ResponseWriter, r *http.Request) {
|
func (api *API) HandleGetFilesInFolder(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -34,14 +35,15 @@ func (api *API) HandleGetFilesInFolder(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
files, err := api.Db.GetFilesInFolder(getFilesInFolderRequest.Folder_id, getFilesInFolderRequest.Limit, getFilesInFolderRequest.Offset)
|
files, folder, err := api.Db.GetFilesInFolder(getFilesInFolderRequest.Folder_id, getFilesInFolderRequest.Limit, getFilesInFolderRequest.Offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.HandleError(w, r, err)
|
api.HandleError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
getFilesInFolderResponse := &GetFilesInFolderResponse{
|
getFilesInFolderResponse := &GetFilesInFolderResponse{
|
||||||
Files: &files,
|
Files: &files,
|
||||||
|
Folder: folder,
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("[api] Get files in folder", getFilesInFolderRequest.Folder_id)
|
log.Println("[api] Get files in folder", getFilesInFolderRequest.Folder_id)
|
||||||
|
|||||||
@@ -23,3 +23,29 @@ func (api *API) HandleGetRandomFiles(w http.ResponseWriter, r *http.Request) {
|
|||||||
log.Println("[api] Get random files")
|
log.Println("[api] Get random files")
|
||||||
json.NewEncoder(w).Encode(getRandomFilesResponse)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
77
pkg/api/handle_manage_file.go
Normal file
77
pkg/api/handle_manage_file.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
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) {
|
||||||
|
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) {
|
||||||
|
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) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
54
pkg/api/handle_manage_folder.go
Normal file
54
pkg/api/handle_manage_folder.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
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) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
215
pkg/api/handle_review.go
Normal file
215
pkg/api/handle_review.go
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
userLevel := api.GetUserLevel(r)
|
||||||
|
if userLevel != database.RoleAdmin {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
51
pkg/api/handle_stat.go
Normal file
51
pkg/api/handle_stat.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"msw-open-music/pkg/database"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RecordPlaybackRequest struct {
|
||||||
|
Playback database.Playback `json:"playback"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) HandleRecordPlayback(w http.ResponseWriter, r *http.Request) {
|
||||||
|
recordPlaybackRequest := &RecordPlaybackRequest{}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(recordPlaybackRequest)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
recordPlaybackRequest.Playback.Time = time.Now()
|
||||||
|
recordPlaybackRequest.Playback.UserID, err = api.GetUserID(w, r)
|
||||||
|
if err != nil {
|
||||||
|
if err == ErrNotLoggedIn {
|
||||||
|
user, err := api.Db.LoginAsAnonymous()
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
recordPlaybackRequest.Playback.UserID = user.ID
|
||||||
|
} else {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("[api] Record playback history",
|
||||||
|
recordPlaybackRequest.Playback.UserID,
|
||||||
|
recordPlaybackRequest.Playback.FileID,
|
||||||
|
recordPlaybackRequest.Playback.Duration,
|
||||||
|
recordPlaybackRequest.Playback.Method)
|
||||||
|
|
||||||
|
err = api.Db.RecordPlayback(recordPlaybackRequest.Playback)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
api.HandleOK(w, r)
|
||||||
|
}
|
||||||
@@ -4,7 +4,9 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
|
"msw-open-music/pkg/database"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -65,13 +67,21 @@ func (api *API) HandleGetFileStream(w http.ResponseWriter, r *http.Request) {
|
|||||||
api.HandleErrorStringCode(w, r, `ffmpeg config not found`, 404)
|
api.HandleErrorStringCode(w, r, `ffmpeg config not found`, 404)
|
||||||
return
|
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, " ")
|
args := strings.Split(ffmpegConfig.Args, " ")
|
||||||
startArgs := []string{"-threads", strconv.FormatInt(api.APIConfig.FfmpegThreads, 10), "-i", path}
|
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(startArgs, args...)
|
||||||
ffmpegArgs = append(ffmpegArgs, endArgs...)
|
ffmpegArgs = append(ffmpegArgs, endArgs...)
|
||||||
cmd := exec.Command("ffmpeg", ffmpegArgs...)
|
cmd := exec.Command("ffmpeg", ffmpegArgs...)
|
||||||
cmd.Stdout = w
|
cmd.Stdout = w
|
||||||
|
// cmd.Stderr = os.Stderr
|
||||||
err = cmd.Run()
|
err = cmd.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.HandleError(w, r, err)
|
api.HandleError(w, r, err)
|
||||||
@@ -85,7 +95,7 @@ type PrepareFileStreamDirectRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PrepareFileStreamDirectResponse struct {
|
type PrepareFileStreamDirectResponse struct {
|
||||||
Filesize int64 `json:"filesize"`
|
File *database.File `json:"file"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// /prepare_file_stream_direct?id=1&config=ffmpeg_config_name
|
// /prepare_file_stream_direct?id=1&config=ffmpeg_config_name
|
||||||
@@ -126,34 +136,29 @@ func (api *API) HandlePrepareFileStreamDirect(w http.ResponseWriter, r *http.Req
|
|||||||
api.HandleErrorStringCode(w, r, `ffmpeg config not found`, 404)
|
api.HandleErrorStringCode(w, r, `ffmpeg config not found`, 404)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
objPath := api.Tmpfs.GetObjFilePath(prepareFileStreamDirectRequst.ID, prepareFileStreamDirectRequst.ConfigName)
|
objPath := api.Tmpfs.GetObjFilePath(prepareFileStreamDirectRequst.ID, ffmpegConfig)
|
||||||
|
|
||||||
// check obj file exists
|
// check obj file exists
|
||||||
exists := api.Tmpfs.Exits(objPath)
|
exists := api.Tmpfs.Exits(objPath)
|
||||||
if exists {
|
if !exists {
|
||||||
fileInfo, err := os.Stat(objPath)
|
// lock the object
|
||||||
|
api.Tmpfs.Lock(objPath)
|
||||||
|
|
||||||
|
args := strings.Split(ffmpegConfig.Args, " ")
|
||||||
|
startArgs := []string{"-threads", strconv.FormatInt(api.APIConfig.FfmpegThreads, 10), "-i", srcPath}
|
||||||
|
endArgs := []string{"-y", objPath}
|
||||||
|
ffmpegArgs := append(startArgs, args...)
|
||||||
|
ffmpegArgs = append(ffmpegArgs, endArgs...)
|
||||||
|
cmd := exec.Command("ffmpeg", ffmpegArgs...)
|
||||||
|
err = cmd.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.HandleError(w, r, err)
|
api.HandleError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
prepareFileStreamDirectResponse := &PrepareFileStreamDirectResponse{
|
|
||||||
Filesize: fileInfo.Size(),
|
|
||||||
}
|
|
||||||
json.NewEncoder(w).Encode(prepareFileStreamDirectResponse)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.Tmpfs.Record(objPath)
|
api.Tmpfs.Record(objPath)
|
||||||
args := strings.Split(ffmpegConfig.Args, " ")
|
api.Tmpfs.Unlock(objPath)
|
||||||
startArgs := []string{"-threads", strconv.FormatInt(api.APIConfig.FfmpegThreads, 10), "-i", srcPath}
|
|
||||||
endArgs := []string{"-vn", "-y", objPath}
|
|
||||||
ffmpegArgs := append(startArgs, args...)
|
|
||||||
ffmpegArgs = append(ffmpegArgs, endArgs...)
|
|
||||||
cmd := exec.Command("ffmpeg", ffmpegArgs...)
|
|
||||||
err = cmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fileInfo, err := os.Stat(objPath)
|
fileInfo, err := os.Stat(objPath)
|
||||||
@@ -161,8 +166,11 @@ func (api *API) HandlePrepareFileStreamDirect(w http.ResponseWriter, r *http.Req
|
|||||||
api.HandleError(w, r, err)
|
api.HandleError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
file.Filesize = fileInfo.Size()
|
||||||
|
|
||||||
prepareFileStreamDirectResponse := &PrepareFileStreamDirectResponse{
|
prepareFileStreamDirectResponse := &PrepareFileStreamDirectResponse{
|
||||||
Filesize: fileInfo.Size(),
|
File: file,
|
||||||
}
|
}
|
||||||
json.NewEncoder(w).Encode(prepareFileStreamDirectResponse)
|
json.NewEncoder(w).Encode(prepareFileStreamDirectResponse)
|
||||||
}
|
}
|
||||||
@@ -180,11 +188,23 @@ func (api *API) HandleGetFileStreamDirect(w http.ResponseWriter, r *http.Request
|
|||||||
configs := q["config"]
|
configs := q["config"]
|
||||||
configName := configs[0]
|
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) {
|
if api.Tmpfs.Exits(path) {
|
||||||
api.Tmpfs.Record(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)
|
log.Println("[api] Get direct cached file", path)
|
||||||
|
|
||||||
http.ServeFile(w, r, path)
|
http.ServeFile(w, r, path)
|
||||||
|
|||||||
140
pkg/api/handle_tag.go
Normal file
140
pkg/api/handle_tag.go
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
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) {
|
||||||
|
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) {
|
||||||
|
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) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
105
pkg/api/handle_tag_and_file.go
Normal file
105
pkg/api/handle_tag_and_file.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
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) {
|
||||||
|
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) {
|
||||||
|
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
|
|
||||||
}
|
|
||||||
326
pkg/api/handle_user.go
Normal file
326
pkg/api/handle_user.go
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
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) HandleLoginAsAnonymous(w http.ResponseWriter, r *http.Request) {
|
||||||
|
user, err := api.LoginAsAnonymous(w, r)
|
||||||
|
resp := &LoginResponse{
|
||||||
|
User: user,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.NewEncoder(w).Encode(resp)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) LoginAsAnonymous(w http.ResponseWriter, r *http.Request) (*database.User, error) {
|
||||||
|
user, err := api.Db.LoginAsAnonymous()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
session, _ := api.store.Get(r, api.defaultSessionName)
|
||||||
|
|
||||||
|
// save session
|
||||||
|
session.Values["userId"] = user.ID
|
||||||
|
err = session.Save(r, w)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// return user
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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) 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) {
|
||||||
|
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) {
|
||||||
|
// middileware reject anonymous user
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// middleware reject anonymous user
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
55
pkg/api/middleware.go
Normal file
55
pkg/api/middleware.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (api *API) PermissionMiddleware(next http.Handler) http.Handler {
|
||||||
|
// 0 anonymous user
|
||||||
|
// 1 admin
|
||||||
|
// 2 normal user
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// get permission of URL
|
||||||
|
permission, ok := api.APIConfig.Permission[r.URL.Path]
|
||||||
|
// 0 means no permission required
|
||||||
|
if !ok || permission == 0 {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ger user permission level
|
||||||
|
userLevel := api.GetUserLevel(r)
|
||||||
|
|
||||||
|
// admin has root (highest) permission level 1
|
||||||
|
if userLevel == 1 {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// anonymous userLevel 0 don't have any permission
|
||||||
|
// check permission level for other users
|
||||||
|
if userLevel == 0 || userLevel > permission {
|
||||||
|
api.HandleError(w, r, errors.New("No enougth permission"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) GetUserLevel(r *http.Request) int64 {
|
||||||
|
session, _ := api.store.Get(r, api.defaultSessionName)
|
||||||
|
userId, ok := session.Values["userId"]
|
||||||
|
if !ok {
|
||||||
|
// not logined user is considered anonymous user
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := api.Db.GetUserById(userId.(int64))
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.Role
|
||||||
|
}
|
||||||
44
pkg/commonconfig/config.go
Normal file
44
pkg/commonconfig/config.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
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"`
|
||||||
|
Permission map[string]int64 `json:"permission"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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,20 +2,46 @@ package database
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"sync"
|
||||||
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/lib/pq"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Database struct {
|
type Database struct {
|
||||||
sqlConn *sql.DB
|
sqlConn *sql.DB
|
||||||
stmt *Stmt
|
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
|
var err error
|
||||||
|
|
||||||
// open database
|
// open database
|
||||||
sqlConn, err := sql.Open("sqlite3", dbName)
|
sqlConn, err := sql.Open("postgres", dbName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -28,8 +54,9 @@ func NewDatabase(dbName string) (*Database, error) {
|
|||||||
|
|
||||||
// new database
|
// new database
|
||||||
database := &Database{
|
database := &Database{
|
||||||
sqlConn: sqlConn,
|
sqlConn: sqlConn,
|
||||||
stmt: stmt,
|
stmt: stmt,
|
||||||
|
singleThreadLock: NewSingleThreadLock(singleThread),
|
||||||
}
|
}
|
||||||
|
|
||||||
return database, nil
|
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"
|
"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) {
|
func (database *Database) GetRandomFiles(limit int64) ([]File, error) {
|
||||||
|
database.singleThreadLock.Lock()
|
||||||
|
defer database.singleThreadLock.Unlock()
|
||||||
|
|
||||||
rows, err := database.stmt.getRandomFiles.Query(limit)
|
rows, err := database.stmt.getRandomFiles.Query(limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -35,8 +30,11 @@ func (database *Database) GetRandomFiles(limit int64) ([]File, error) {
|
|||||||
return files, nil
|
return files, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (database *Database) GetFilesInFolder(folder_id int64, limit int64, offset int64) ([]File, error) {
|
func (database *Database) GetRandomFilesWithTag(tagID, limit int64) ([]File, error) {
|
||||||
rows, err := database.stmt.getFilesInFolder.Query(folder_id, limit, offset)
|
database.singleThreadLock.Lock()
|
||||||
|
defer database.singleThreadLock.Unlock()
|
||||||
|
|
||||||
|
rows, err := database.stmt.getRandomFilesWithTag.Query(tagID, limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -44,10 +42,9 @@ func (database *Database) GetFilesInFolder(folder_id int64, limit int64, offset
|
|||||||
files := make([]File, 0)
|
files := make([]File, 0)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
file := File{
|
file := File{
|
||||||
Db: database,
|
Db: database,
|
||||||
Folder_id: folder_id,
|
|
||||||
}
|
}
|
||||||
err = rows.Scan(&file.ID, &file.Filename, &file.Filesize, &file.Foldername)
|
err = rows.Scan(&file.ID, &file.Folder_id, &file.Filename, &file.Foldername, &file.Filesize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -56,7 +53,35 @@ func (database *Database) GetFilesInFolder(folder_id int64, limit int64, offset
|
|||||||
return files, nil
|
return files, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (database *Database) GetFilesInFolder(folder_id int64, limit int64, offset int64) ([]File, string, error) {
|
||||||
|
database.singleThreadLock.Lock()
|
||||||
|
defer database.singleThreadLock.Unlock()
|
||||||
|
|
||||||
|
rows, err := database.stmt.getFilesInFolder.Query(folder_id, limit, offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
files := make([]File, 0)
|
||||||
|
folder := ""
|
||||||
|
for rows.Next() {
|
||||||
|
file := File{
|
||||||
|
Db: database,
|
||||||
|
Folder_id: folder_id,
|
||||||
|
}
|
||||||
|
err = rows.Scan(&file.ID, &file.Filename, &file.Filesize, &file.Foldername, &folder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
files = append(files, file)
|
||||||
|
}
|
||||||
|
return files, folder, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (database *Database) SearchFolders(foldername string, limit int64, offset int64) ([]Folder, error) {
|
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)
|
rows, err := database.stmt.searchFolders.Query("%"+foldername+"%", limit, offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("Error searching folders at query " + err.Error())
|
return nil, errors.New("Error searching folders at query " + err.Error())
|
||||||
@@ -80,7 +105,11 @@ func (database *Database) GetFile(id int64) (*File, error) {
|
|||||||
file := &File{
|
file := &File{
|
||||||
Db: database,
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -115,13 +144,33 @@ func (database *Database) ResetFolder() error {
|
|||||||
return err
|
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)
|
patternDict := make(map[string]bool)
|
||||||
for _, v := range pattern {
|
for _, v := range pattern {
|
||||||
patternDict[v] = true
|
patternDict[v] = true
|
||||||
}
|
}
|
||||||
log.Println("[db] Walk", root, patternDict)
|
log.Println("[db] Walk", root, patternDict)
|
||||||
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
|
||||||
|
tags := make([]*Tag, 0)
|
||||||
|
for _, tagID := range tagIDs {
|
||||||
|
tag, err := database.GetTag(tagID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tags = append(tags, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := database.sqlConn.Begin()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
insertFolderStmt := tx.Stmt(database.stmt.insertFolder)
|
||||||
|
insertFileStmt := tx.Stmt(database.stmt.insertFile)
|
||||||
|
putTagOnFileStmt := tx.Stmt(database.stmt.putTagOnFile)
|
||||||
|
findFolderStmt := tx.Stmt(database.stmt.findFolder)
|
||||||
|
findFileStmt := tx.Stmt(database.stmt.findFile)
|
||||||
|
|
||||||
|
err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -136,19 +185,76 @@ func (database *Database) Walk(root string, pattern []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// insert file, folder will aut created
|
// insert file and folder
|
||||||
err = database.Insert(path, info.Size())
|
// fileID, err := database.Insert(path, info.Size())
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
var folderID int64
|
||||||
|
folder, filename := filepath.Split(path)
|
||||||
|
err = findFolderStmt.QueryRow(folder).Scan(&folderID)
|
||||||
|
if err != nil {
|
||||||
|
result, err := insertFolderStmt.Query(folder, filepath.Base(folder))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for result.Next() {
|
||||||
|
err = result.Scan(&folderID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// try find file id
|
||||||
|
var fileID int64
|
||||||
|
result, err := findFileStmt.Query(folderID, filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
for result.Next() {
|
||||||
|
err = result.Scan(&fileID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert new file
|
||||||
|
result, err = insertFileStmt.Query(folderID, filename, filename, info.Size())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for result.Next() {
|
||||||
|
err = result.Scan(&fileID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tag := range tags {
|
||||||
|
_, err := putTagOnFileStmt.Exec(tag.ID, fileID, userID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (database *Database) GetFolder(folderId int64) (*Folder, error) {
|
func (database *Database) GetFolder(folderId int64) (*Folder, error) {
|
||||||
folder := &Folder{
|
folder := &Folder{
|
||||||
Db: database,
|
Db: database,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
database.singleThreadLock.Lock()
|
||||||
|
defer database.singleThreadLock.Unlock()
|
||||||
|
|
||||||
err := database.stmt.getFolder.QueryRow(folderId).Scan(&folder.Folder)
|
err := database.stmt.getFolder.QueryRow(folderId).Scan(&folder.Folder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -157,6 +263,9 @@ func (database *Database) GetFolder(folderId int64) (*Folder, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (database *Database) SearchFiles(filename string, limit int64, offset int64) ([]File, 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)
|
rows, err := database.stmt.searchFiles.Query("%"+filename+"%", limit, offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("Error searching files at query " + err.Error())
|
return nil, errors.New("Error searching files at query " + err.Error())
|
||||||
@@ -181,6 +290,10 @@ func (database *Database) SearchFiles(filename string, limit int64, offset int64
|
|||||||
|
|
||||||
func (database *Database) FindFolder(folder string) (int64, error) {
|
func (database *Database) FindFolder(folder string) (int64, error) {
|
||||||
var id int64
|
var id int64
|
||||||
|
|
||||||
|
database.singleThreadLock.Lock()
|
||||||
|
defer database.singleThreadLock.Unlock()
|
||||||
|
|
||||||
err := database.stmt.findFolder.QueryRow(folder).Scan(&id)
|
err := database.stmt.findFolder.QueryRow(folder).Scan(&id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@@ -190,6 +303,10 @@ func (database *Database) FindFolder(folder string) (int64, error) {
|
|||||||
|
|
||||||
func (database *Database) FindFile(folderId int64, filename string) (int64, error) {
|
func (database *Database) FindFile(folderId int64, filename string) (int64, error) {
|
||||||
var id int64
|
var id int64
|
||||||
|
|
||||||
|
database.singleThreadLock.Lock()
|
||||||
|
defer database.singleThreadLock.Unlock()
|
||||||
|
|
||||||
err := database.stmt.findFile.QueryRow(folderId, filename).Scan(&id)
|
err := database.stmt.findFile.QueryRow(folderId, filename).Scan(&id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@@ -198,6 +315,10 @@ func (database *Database) FindFile(folderId int64, filename string) (int64, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (database *Database) InsertFolder(folder string) (int64, error) {
|
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))
|
result, err := database.stmt.insertFolder.Exec(folder, filepath.Base(folder))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@@ -209,31 +330,125 @@ func (database *Database) InsertFolder(folder string) (int64, error) {
|
|||||||
return lastInsertId, nil
|
return lastInsertId, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (database *Database) InsertFile(folderId int64, filename string, filesize int64) error {
|
func (database *Database) InsertFile(folderId int64, filename string, filesize int64) (int64, error) {
|
||||||
_, err := database.stmt.insertFile.Exec(folderId, filename, filesize)
|
database.singleThreadLock.Lock()
|
||||||
|
defer database.singleThreadLock.Unlock()
|
||||||
|
|
||||||
|
result, err := database.stmt.insertFile.Exec(folderId, filename, filename, filesize)
|
||||||
if err != nil {
|
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)
|
folder, filename := filepath.Split(path)
|
||||||
folderId, err := database.FindFolder(folder)
|
folderId, err := database.FindFolder(folder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
folderId, err = database.InsertFolder(folder)
|
folderId, err = database.InsertFolder(folder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return 0, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if file exists, skip it
|
// if file exists, skip it
|
||||||
_, err = database.FindFile(folderId, filename)
|
lastInsertId, err := database.FindFile(folderId, filename)
|
||||||
if err == nil {
|
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 {
|
if err != nil {
|
||||||
return err
|
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
|
||||||
|
}
|
||||||
7
pkg/database/method_playback.go
Normal file
7
pkg/database/method_playback.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
func (database *Database) RecordPlayback(playback Playback) error {
|
||||||
|
_, err := database.stmt.recordPlaybackStmt.Exec(
|
||||||
|
playback.UserID, playback.FileID, playback.Time, playback.Method, playback.Duration)
|
||||||
|
return err
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
116
pkg/database/method_tag.go
Normal file
116
pkg/database/method_tag.go
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
func (database *Database) InsertTag(tag *Tag) (int64, error) {
|
||||||
|
database.singleThreadLock.Lock()
|
||||||
|
defer database.singleThreadLock.Unlock()
|
||||||
|
|
||||||
|
result, err := database.stmt.insertTag.Query(tag.Name, tag.Description, tag.CreatedByUserId)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
var id int64
|
||||||
|
for result.Next() {
|
||||||
|
err = result.Scan(&id)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -2,98 +2,348 @@ package database
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var initFilesTableQuery = `CREATE TABLE IF NOT EXISTS files (
|
var initFilesTableQuery = `CREATE TABLE IF NOT EXISTS files (
|
||||||
id INTEGER PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
folder_id INTEGER NOT NULL,
|
folder_id INTEGER NOT NULL REFERENCES folders(id),
|
||||||
|
realname TEXT NOT NULL,
|
||||||
filename TEXT NOT NULL,
|
filename TEXT NOT NULL,
|
||||||
filesize INTEGER NOT NULL
|
filesize INTEGER NOT NULL,
|
||||||
|
UNIQUE (folder_id, realname)
|
||||||
);`
|
);`
|
||||||
|
|
||||||
var initFoldersTableQuery = `CREATE TABLE IF NOT EXISTS folders (
|
var initFoldersTableQuery = `CREATE TABLE IF NOT EXISTS folders (
|
||||||
id INTEGER PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
folder TEXT NOT NULL,
|
folder TEXT NOT NULL UNIQUE,
|
||||||
foldername TEXT NOT NULL
|
foldername TEXT NOT NULL
|
||||||
);`
|
);`
|
||||||
|
|
||||||
var initFeedbacksTableQuery = `CREATE TABLE IF NOT EXISTS feedbacks (
|
var initFeedbacksTableQuery = `CREATE TABLE IF NOT EXISTS feedbacks (
|
||||||
id INTEGER PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
time INTEGER NOT NULL,
|
time INTEGER NOT NULL,
|
||||||
feedback TEXT NOT NULL,
|
content TEXT NOT NULL,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
header TEXT NOT NULL
|
header TEXT NOT NULL
|
||||||
);`
|
);`
|
||||||
|
|
||||||
|
// User table schema definition
|
||||||
|
// role: 0 - Anonymous User, 1 - Admin, 2 - User
|
||||||
|
// postgres avatar references problem
|
||||||
|
var initUsersTableQuery = `CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
username TEXT NOT NULL UNIQUE,
|
||||||
|
password TEXT NOT NULL,
|
||||||
|
role INTEGER NOT NULL,
|
||||||
|
active BOOLEAN NOT NULL,
|
||||||
|
avatar_id INTEGER NOT NULL DEFAULT 0
|
||||||
|
);`
|
||||||
|
|
||||||
|
var initAvatarsTableQuery = `CREATE TABLE IF NOT EXISTS avatars (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
avatarname TEXT NOT NULL,
|
||||||
|
avatar BYTEA NOT NULL
|
||||||
|
);`
|
||||||
|
|
||||||
|
var initTagsTableQuery = `CREATE TABLE IF NOT EXISTS tags (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
created_by_user_id INTEGER NOT NULL REFERENCES users(id)
|
||||||
|
);`
|
||||||
|
|
||||||
|
var initFileHasTagTableQuery = `CREATE TABLE IF NOT EXISTS file_has_tag (
|
||||||
|
file_id INTEGER NOT NULL REFERENCES files(id),
|
||||||
|
tag_id INTEGER NOT NULL REFERENCES tags(id),
|
||||||
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||||
|
PRIMARY KEY (file_id, tag_id)
|
||||||
|
);`
|
||||||
|
|
||||||
|
var initLikesTableQuery = `CREATE TABLE IF NOT EXISTS likes (
|
||||||
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||||
|
file_id INTEGER NOT NULL REFERENCES files(id),
|
||||||
|
PRIMARY KEY (user_id, file_id)
|
||||||
|
);`
|
||||||
|
|
||||||
|
var initReviewsTableQuery = `CREATE TABLE IF NOT EXISTS reviews (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||||
|
file_id INTEGER NOT NULL REFERENCES files(id),
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL DEFAULT 0,
|
||||||
|
content TEXT NOT NULL
|
||||||
|
);`
|
||||||
|
|
||||||
|
var initPlaybacksTableQuery = `CREATE TABLE IF NOT EXISTS playbacks (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||||
|
file_id INTEGER NOT NULL REFERENCES files(id),
|
||||||
|
time TIMESTAMP NOT NULL,
|
||||||
|
method INTEGER NOT NULL,
|
||||||
|
duration INTERVAL NOT NULL
|
||||||
|
);`
|
||||||
|
|
||||||
|
var initLogsTableQuery = `CREATE TABLE IF NOT EXISTS logs (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
time INTEGER NOT NULL,
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
user_id INTEGER NOT NULL REFERENCES users(id)
|
||||||
|
);`
|
||||||
|
|
||||||
|
var initTmpfsTableQuery = `CREATE TABLE IF NOT EXISTS tmpfs (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
path TEXT NOT NULL,
|
||||||
|
size INTEGER NOT NULL,
|
||||||
|
file_id INTEGER NOT NULL REFERENCES files(id),
|
||||||
|
ffmpeg_config TEXT NOT NULL,
|
||||||
|
created_time INTEGER NOT NULL,
|
||||||
|
accessed_time INTEGER NOT NULL
|
||||||
|
);`
|
||||||
|
|
||||||
var insertFolderQuery = `INSERT INTO folders (folder, foldername)
|
var insertFolderQuery = `INSERT INTO folders (folder, foldername)
|
||||||
VALUES (?, ?);`
|
VALUES ($1, $2)
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
|
RETURNING id;
|
||||||
|
;`
|
||||||
|
|
||||||
var findFolderQuery = `SELECT id FROM folders WHERE folder = ? LIMIT 1;`
|
var findFolderQuery = `SELECT id FROM folders WHERE folder = $1 LIMIT 1;`
|
||||||
|
|
||||||
var findFileQuery = `SELECT id FROM files WHERE folder_id = ? AND filename = ? LIMIT 1;`
|
var findFileQuery = `SELECT id FROM files WHERE folder_id = $1 AND realname = $2 LIMIT 1;`
|
||||||
|
|
||||||
var insertFileQuery = `INSERT INTO files (folder_id, filename, filesize)
|
var insertFileQuery = `INSERT INTO files (folder_id, realname, filename, filesize)
|
||||||
VALUES (?, ?, ?);`
|
VALUES ($1, $2, $3, $4)
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
|
RETURNING id;`
|
||||||
|
|
||||||
var searchFilesQuery = `SELECT
|
var searchFilesQuery = `SELECT
|
||||||
files.id, files.folder_id, files.filename, folders.foldername, files.filesize
|
files.id, files.folder_id, files.filename, folders.foldername, files.filesize
|
||||||
FROM files
|
FROM files
|
||||||
JOIN folders ON files.folder_id = folders.id
|
JOIN folders ON files.folder_id = folders.id
|
||||||
WHERE filename LIKE ?
|
WHERE filename ILIKE $1
|
||||||
LIMIT ? OFFSET ?;`
|
ORDER BY folders.foldername, files.filename
|
||||||
|
LIMIT $2 OFFSET $3;`
|
||||||
|
|
||||||
var getFolderQuery = `SELECT folder FROM folders WHERE id = ? LIMIT 1;`
|
var getFolderQuery = `SELECT folder FROM folders WHERE id = $1 LIMIT 1;`
|
||||||
|
|
||||||
var dropFilesQuery = `DROP TABLE files;`
|
var dropFilesQuery = `DROP TABLE files;`
|
||||||
|
|
||||||
var dropFolderQuery = `DROP TABLE folders;`
|
var dropFolderQuery = `DROP TABLE folders;`
|
||||||
|
|
||||||
var getFileQuery = `SELECT
|
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
|
FROM files
|
||||||
JOIN folders ON files.folder_id = folders.id
|
JOIN folders ON files.folder_id = folders.id
|
||||||
WHERE files.id = ?
|
WHERE files.id = $1
|
||||||
LIMIT 1;`
|
LIMIT 1;`
|
||||||
|
|
||||||
var searchFoldersQuery = `SELECT
|
var searchFoldersQuery = `SELECT
|
||||||
id, folder, foldername
|
id, folder, foldername
|
||||||
FROM folders
|
FROM folders
|
||||||
WHERE foldername LIKE ?
|
WHERE foldername ILIKE $1
|
||||||
LIMIT ? OFFSET ?;`
|
ORDER BY foldername
|
||||||
|
LIMIT $2 OFFSET $3;`
|
||||||
|
|
||||||
var getFilesInFolderQuery = `SELECT
|
var getFilesInFolderQuery = `SELECT
|
||||||
files.id, files.filename, files.filesize, folders.foldername
|
files.id, files.filename, files.filesize, folders.foldername, folders.folder
|
||||||
FROM files
|
FROM files
|
||||||
JOIN folders ON files.folder_id = folders.id
|
JOIN folders ON files.folder_id = folders.id
|
||||||
WHERE folder_id = ?
|
WHERE folder_id = $1
|
||||||
LIMIT ? OFFSET ?;`
|
ORDER BY files.filename
|
||||||
|
LIMIT $2 OFFSET $3;`
|
||||||
|
|
||||||
var getRandomFilesQuery = `SELECT
|
var getRandomFilesQuery = `SELECT
|
||||||
files.id, files.folder_id, files.filename, folders.foldername, files.filesize
|
files.id, files.folder_id, files.filename, folders.foldername, files.filesize
|
||||||
FROM files
|
FROM files
|
||||||
JOIN folders ON files.folder_id = folders.id
|
JOIN folders ON files.folder_id = folders.id
|
||||||
ORDER BY RANDOM()
|
ORDER BY RANDOM()
|
||||||
LIMIT ?;`
|
LIMIT $1;`
|
||||||
|
|
||||||
var insertFeedbackQuery = `INSERT INTO feedbacks (time, feedback, header)
|
var getRandomFilesWithTagQuery = `SELECT
|
||||||
VALUES (?, ?, ?);`
|
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 = $1
|
||||||
|
ORDER BY RANDOM()
|
||||||
|
LIMIT $2;`
|
||||||
|
|
||||||
|
var insertFeedbackQuery = `INSERT INTO feedbacks (time, content, user_id, header)
|
||||||
|
VALUES ($1, $2, $3, $4);`
|
||||||
|
|
||||||
|
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 = $1;`
|
||||||
|
|
||||||
|
var insertUserQuery = `INSERT INTO users (username, password, role, active, avatar_id)
|
||||||
|
VALUES ($1, $2, $3, $4, $5);`
|
||||||
|
|
||||||
|
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 = $1 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 = $1 LIMIT 1;`
|
||||||
|
|
||||||
|
var updateUserActiveQuery = `UPDATE users SET active = $1 WHERE id = $2;`
|
||||||
|
|
||||||
|
var updateUsernameQuery = `UPDATE users SET username = $1 WHERE id = $2;`
|
||||||
|
|
||||||
|
var updateUserPasswordQuery = `UPDATE users SET password = $1 WHERE id = $2;`
|
||||||
|
|
||||||
|
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 ($1, $2, $3) RETURNING id;`
|
||||||
|
|
||||||
|
var deleteTagQuery = `DELETE FROM tags WHERE id = $1;`
|
||||||
|
|
||||||
|
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 = $1 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 = $1, description = $2 WHERE id = $3;`
|
||||||
|
|
||||||
|
// postgres INSERT IGNORE
|
||||||
|
var putTagOnFileQuery = `INSERT INTO file_has_tag (tag_id, file_id, user_id) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING;`
|
||||||
|
|
||||||
|
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 = $1
|
||||||
|
ORDER BY tags.name
|
||||||
|
;`
|
||||||
|
|
||||||
|
var deleteTagOnFileQuery = `DELETE FROM file_has_tag WHERE tag_id = $1 AND file_id = $2;`
|
||||||
|
|
||||||
|
var deleteTagReferenceInFileHasTagQuery = `DELETE FROM file_has_tag WHERE tag_id = $1;`
|
||||||
|
|
||||||
|
var updateFoldernameQuery = `UPDATE folders SET foldername = $1 WHERE id = $2;`
|
||||||
|
|
||||||
|
var insertReviewQuery = `INSERT INTO reviews (user_id, file_id, created_at, content)
|
||||||
|
VALUES ($1, $2, $3, $4);`
|
||||||
|
|
||||||
|
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 = $1
|
||||||
|
ORDER BY reviews.created_at
|
||||||
|
;`
|
||||||
|
|
||||||
|
var getReviewQuery = `SELECT id, file_id, user_id, created_at, updated_at, content FROM reviews WHERE id = $1 LIMIT 1;`
|
||||||
|
|
||||||
|
var updateReviewQuery = `UPDATE reviews SET content = $1, updated_at = $2 WHERE id = $3;`
|
||||||
|
|
||||||
|
var deleteReviewQuery = `DELETE FROM reviews WHERE id = $1;`
|
||||||
|
|
||||||
|
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 = $1
|
||||||
|
ORDER BY reviews.created_at
|
||||||
|
;`
|
||||||
|
|
||||||
|
var deleteFileQuery = `DELETE FROM files WHERE id = $1;`
|
||||||
|
|
||||||
|
var deleteFileReferenceInFileHasTagQuery = `DELETE FROM file_has_tag WHERE file_id = $1;`
|
||||||
|
|
||||||
|
var deleteFileReferenceInReviewsQuery = `DELETE FROM reviews WHERE file_id = $1;`
|
||||||
|
|
||||||
|
var updateFilenameQuery = `UPDATE files SET filename = $1 WHERE id = $2;`
|
||||||
|
|
||||||
|
var resetFilenameQuery = `UPDATE files SET filename = realname WHERE id = $1;`
|
||||||
|
|
||||||
|
var recordPlaybackQuery = `INSERT INTO playbacks (user_id, file_id, time, method, duration) VALUES ($1, $2, $3, $4, $5);`
|
||||||
|
|
||||||
type Stmt struct {
|
type Stmt struct {
|
||||||
initFilesTable *sql.Stmt
|
initFilesTable *sql.Stmt
|
||||||
initFoldersTable *sql.Stmt
|
initFoldersTable *sql.Stmt
|
||||||
initFeedbacksTable *sql.Stmt
|
initFeedbacksTable *sql.Stmt
|
||||||
insertFolder *sql.Stmt
|
initUsersTable *sql.Stmt
|
||||||
insertFile *sql.Stmt
|
initAvatarsTable *sql.Stmt
|
||||||
findFolder *sql.Stmt
|
initTagsTable *sql.Stmt
|
||||||
findFile *sql.Stmt
|
initFileHasTag *sql.Stmt
|
||||||
searchFiles *sql.Stmt
|
initLikesTable *sql.Stmt
|
||||||
getFolder *sql.Stmt
|
initReviewsTable *sql.Stmt
|
||||||
dropFiles *sql.Stmt
|
initPlaybacksTable *sql.Stmt
|
||||||
dropFolder *sql.Stmt
|
initLogsTable *sql.Stmt
|
||||||
getFile *sql.Stmt
|
initTmpfsTable *sql.Stmt
|
||||||
searchFolders *sql.Stmt
|
insertFolder *sql.Stmt
|
||||||
getFilesInFolder *sql.Stmt
|
insertFile *sql.Stmt
|
||||||
getRandomFiles *sql.Stmt
|
findFolder *sql.Stmt
|
||||||
insertFeedback *sql.Stmt
|
findFile *sql.Stmt
|
||||||
|
searchFiles *sql.Stmt
|
||||||
|
getFolder *sql.Stmt
|
||||||
|
dropFiles *sql.Stmt
|
||||||
|
dropFolder *sql.Stmt
|
||||||
|
getFile *sql.Stmt
|
||||||
|
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
|
||||||
|
recordPlaybackStmt *sql.Stmt
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) {
|
func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) {
|
||||||
@@ -101,14 +351,42 @@ func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) {
|
|||||||
|
|
||||||
stmt := &Stmt{}
|
stmt := &Stmt{}
|
||||||
|
|
||||||
|
// init folders table
|
||||||
|
stmt.initFoldersTable, err = sqlConn.Prepare(initFoldersTableQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = stmt.initFoldersTable.Exec()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// init files table
|
// init files table
|
||||||
stmt.initFilesTable, err = sqlConn.Prepare(initFilesTableQuery)
|
stmt.initFilesTable, err = sqlConn.Prepare(initFilesTableQuery)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
_, err = stmt.initFilesTable.Exec()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// init folders table
|
// init avatars table
|
||||||
stmt.initFoldersTable, err = sqlConn.Prepare(initFoldersTableQuery)
|
stmt.initAvatarsTable, err = sqlConn.Prepare(initAvatarsTableQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = stmt.initAvatarsTable.Exec()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// init users table
|
||||||
|
stmt.initUsersTable, err = sqlConn.Prepare(initUsersTableQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = stmt.initUsersTable.Exec()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -118,21 +396,83 @@ func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// run init statement
|
|
||||||
_, err = stmt.initFilesTable.Exec()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = stmt.initFoldersTable.Exec()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = stmt.initFeedbacksTable.Exec()
|
_, err = stmt.initFeedbacksTable.Exec()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// init tags table
|
||||||
|
stmt.initTagsTable, err = sqlConn.Prepare(initTagsTableQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = stmt.initTagsTable.Exec()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// init file_has_tag table
|
||||||
|
stmt.initFileHasTag, err = sqlConn.Prepare(initFileHasTagTableQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = stmt.initFileHasTag.Exec()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// init likes table
|
||||||
|
stmt.initLikesTable, err = sqlConn.Prepare(initLikesTableQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = stmt.initLikesTable.Exec()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// init reviews table
|
||||||
|
stmt.initReviewsTable, err = sqlConn.Prepare(initReviewsTableQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = stmt.initReviewsTable.Exec()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// init playbacks table
|
||||||
|
stmt.initPlaybacksTable, err = sqlConn.Prepare(initPlaybacksTableQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = stmt.initPlaybacksTable.Exec()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// init logs table
|
||||||
|
stmt.initLogsTable, err = sqlConn.Prepare(initLogsTableQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = stmt.initLogsTable.Exec()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// init tmpfs table
|
||||||
|
stmt.initTmpfsTable, err = sqlConn.Prepare(initTmpfsTableQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = stmt.initTmpfsTable.Exec()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Init tables finished")
|
||||||
|
|
||||||
// init insert folder statement
|
// init insert folder statement
|
||||||
stmt.insertFolder, err = sqlConn.Prepare(insertFolderQuery)
|
stmt.insertFolder, err = sqlConn.Prepare(insertFolderQuery)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -205,11 +545,238 @@ func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// init getRandomFilesWithTag
|
||||||
|
stmt.getRandomFilesWithTag, err = sqlConn.Prepare(getRandomFilesWithTagQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// init insertFeedback
|
// init insertFeedback
|
||||||
stmt.insertFeedback, err = sqlConn.Prepare(insertFeedbackQuery)
|
stmt.insertFeedback, err = sqlConn.Prepare(insertFeedbackQuery)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt.recordPlaybackStmt, err = sqlConn.Prepare(recordPlaybackQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Init statements finished")
|
||||||
|
|
||||||
return stmt, err
|
return stmt, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,29 +2,97 @@ package database
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type File struct {
|
type File struct {
|
||||||
Db *Database `json:"-"`
|
Db *Database `json:"-"`
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Folder_id int64 `json:"folder_id"`
|
Folder_id int64 `json:"folder_id"`
|
||||||
Foldername string `json:"foldername"`
|
Foldername string `json:"foldername"`
|
||||||
Filename string `json:"filename"`
|
Realname string `json:"-"`
|
||||||
Filesize int64 `json:"filesize"`
|
Filename string `json:"filename"`
|
||||||
|
Filesize int64 `json:"filesize"`
|
||||||
|
folderCache *Folder
|
||||||
}
|
}
|
||||||
|
|
||||||
type Folder struct {
|
type Folder struct {
|
||||||
Db *Database `json:"-"`
|
Db *Database `json:"-"`
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Folder string `json:"-"`
|
Folder string `json:"folder"`
|
||||||
Foldername string `json:"foldername"`
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Playback struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
FileID int64 `json:"file_id"`
|
||||||
|
Time time.Time `json:"time"`
|
||||||
|
Method int64 `json:"method"`
|
||||||
|
Duration time.Duration `json:"Duration"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
RoleAnonymous = int64(0)
|
||||||
|
RoleAdmin = int64(1)
|
||||||
|
RoleUser = int64(2)
|
||||||
|
)
|
||||||
|
|
||||||
func (f *File) Path() (string, error) {
|
func (f *File) Path() (string, error) {
|
||||||
folder, err := f.Db.GetFolder(f.Folder_id)
|
var err error
|
||||||
|
if f.folderCache == nil {
|
||||||
|
f.folderCache, err = f.Db.GetFolder(f.Folder_id)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return filepath.Join(folder.Folder, f.Filename), nil
|
return filepath.Join(f.folderCache.Folder, f.Realname), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *File) Dir() (string, error) {
|
||||||
|
var err error
|
||||||
|
if f.folderCache == nil {
|
||||||
|
f.folderCache, err = f.Db.GetFolder(f.Folder_id)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return f.folderCache.Folder, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package tmpfs
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
"msw-open-music/pkg/commonconfig"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -10,30 +11,43 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Tmpfs struct {
|
type Tmpfs struct {
|
||||||
record map[string]int64
|
record map[string]int64
|
||||||
Config TmpfsConfig
|
Config commonconfig.TmpfsConfig
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
|
recordLocks map[string]*sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tmpfs *Tmpfs) GetObjFilePath(id int64, configName string) (string) {
|
func (tmpfs *Tmpfs) GetObjFilePath(id int64, ffmpegConfig commonconfig.FfmpegConfig) string {
|
||||||
return filepath.Join(tmpfs.Config.Root, strconv.FormatInt(id, 10) + "." + configName + ".ogg")
|
return filepath.Join(tmpfs.Config.Root, strconv.FormatInt(id, 10)+"."+ffmpegConfig.Name+"."+ffmpegConfig.Format)
|
||||||
}
|
}
|
||||||
|
|
||||||
type TmpfsConfig struct {
|
func (tmpfs *Tmpfs) GetLock(filename string) *sync.Mutex {
|
||||||
FileLifeTime int64 `json:"file_life_time"`
|
if _, ok := tmpfs.recordLocks[filename]; !ok {
|
||||||
CleanerInternal int64 `json:"cleaner_internal"`
|
tmpfs.recordLocks[filename] = &sync.Mutex{}
|
||||||
Root string `json:"root"`
|
}
|
||||||
|
return tmpfs.recordLocks[filename]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTmpfsConfig() (*TmpfsConfig) {
|
func (tmpfs *Tmpfs) Lock(filename string) {
|
||||||
config := &TmpfsConfig{}
|
tmpfs.GetLock(filename).Lock()
|
||||||
return config
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTmpfs(config TmpfsConfig) *Tmpfs {
|
func (tmpfs *Tmpfs) Unlock(filename string) {
|
||||||
|
tmpfs.GetLock(filename).Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTmpfs(config commonconfig.TmpfsConfig) *Tmpfs {
|
||||||
tmpfs := &Tmpfs{
|
tmpfs := &Tmpfs{
|
||||||
record: make(map[string]int64),
|
record: make(map[string]int64),
|
||||||
Config: config,
|
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)
|
tmpfs.wg.Add(1)
|
||||||
go tmpfs.Cleaner()
|
go tmpfs.Cleaner()
|
||||||
@@ -44,7 +58,7 @@ func (tmpfs *Tmpfs) Record(filename string) {
|
|||||||
tmpfs.record[filename] = time.Now().Unix()
|
tmpfs.record[filename] = time.Now().Unix()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tmpfs *Tmpfs) Exits(filename string) (bool) {
|
func (tmpfs *Tmpfs) Exits(filename string) bool {
|
||||||
_, ok := tmpfs.record[filename]
|
_, ok := tmpfs.record[filename]
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
@@ -53,15 +67,23 @@ func (tmpfs *Tmpfs) Cleaner() {
|
|||||||
var err error
|
var err error
|
||||||
for {
|
for {
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
for key, value := range tmpfs.record {
|
for path, lock := range tmpfs.recordLocks {
|
||||||
if now - value > tmpfs.Config.FileLifeTime {
|
lock.Lock()
|
||||||
err = os.Remove(key)
|
recordTime, ok := tmpfs.record[path]
|
||||||
|
if !ok {
|
||||||
|
lock.Unlock()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if now-recordTime > tmpfs.Config.FileLifeTime {
|
||||||
|
err = os.Remove(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("[tmpfs] Failed to remove file", err)
|
log.Println("[tmpfs] Failed to remove file", err)
|
||||||
}
|
}
|
||||||
log.Println("[tmpfs] Deleted file", key)
|
log.Println("[tmpfs] Deleted file", path)
|
||||||
delete(tmpfs.record, key)
|
delete(tmpfs.record, path)
|
||||||
|
delete(tmpfs.recordLocks, path)
|
||||||
}
|
}
|
||||||
|
lock.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
|
|||||||
@@ -1,70 +1,9 @@
|
|||||||
# Getting Started with Create React App
|
# MSW Open Music Web Frontend
|
||||||
|
|
||||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
This is a React single page application. And use Preact instead of React to achieve a smaller file size.
|
||||||
|
|
||||||
## Available Scripts
|
`node_modules` only has 19M. We uses esbuild and shell scripts and build only takes a milliseconds!
|
||||||
|
|
||||||
In the project directory, you can run:
|
## How to build
|
||||||
|
|
||||||
### `npm start`
|
Simple run `./build.sh`, then all output files are under `./build/` directory.
|
||||||
|
|
||||||
Runs the app in the development mode.\
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
|
||||||
|
|
||||||
The page will reload if you make edits.\
|
|
||||||
You will also see any lint errors in the console.
|
|
||||||
|
|
||||||
### `npm test`
|
|
||||||
|
|
||||||
Launches the test runner in the interactive watch mode.\
|
|
||||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
|
||||||
|
|
||||||
### `npm run build`
|
|
||||||
|
|
||||||
Builds the app for production to the `build` folder.\
|
|
||||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
|
||||||
|
|
||||||
The build is minified and the filenames include the hashes.\
|
|
||||||
Your app is ready to be deployed!
|
|
||||||
|
|
||||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
|
||||||
|
|
||||||
### `npm run eject`
|
|
||||||
|
|
||||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
|
||||||
|
|
||||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
|
||||||
|
|
||||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
|
||||||
|
|
||||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
|
||||||
|
|
||||||
## Learn More
|
|
||||||
|
|
||||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
|
||||||
|
|
||||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
|
||||||
|
|
||||||
### Code Splitting
|
|
||||||
|
|
||||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
|
||||||
|
|
||||||
### Analyzing the Bundle Size
|
|
||||||
|
|
||||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
|
||||||
|
|
||||||
### Making a Progressive Web App
|
|
||||||
|
|
||||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
|
||||||
|
|
||||||
### Advanced Configuration
|
|
||||||
|
|
||||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
|
||||||
|
|
||||||
### Deployment
|
|
||||||
|
|
||||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
|
||||||
|
|
||||||
### `npm run build` fails to minify
|
|
||||||
|
|
||||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
|
||||||
|
|||||||
6
web/build.sh
Executable file
6
web/build.sh
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
rm -rf build
|
||||||
|
cp -raf public build
|
||||||
|
./node_modules/.bin/esbuild src/index.jsx --bundle --outfile=build/msw-open-music.js --alias:react=preact/compat --alias:react-dom=preact/compat --minify --analyze
|
||||||
|
cat public/index.html | sed "s/%PUBLIC_URL%/$PUBLIC_URL/" > build/index.html
|
||||||
|
|
||||||
|
echo "Build done, output files under ./build directory"
|
||||||
37950
web/package-lock.json
generated
37950
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,44 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "msw-open-music-react",
|
"name": "msw-open-music-react",
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@testing-library/jest-dom": "^5.15.0",
|
"@preact/compat": "^17.1.2",
|
||||||
"@testing-library/react": "^11.2.7",
|
"esbuild": "^0.15.17",
|
||||||
"@testing-library/user-event": "^12.8.3",
|
"react-router-dom": "^6.4.4",
|
||||||
"react": "^17.0.2",
|
"water.css": "^2.1.1"
|
||||||
"react-dom": "^17.0.2",
|
|
||||||
"react-router": "^6.0.2",
|
|
||||||
"react-router-dom": "^6.0.2",
|
|
||||||
"react-scripts": "4.0.3",
|
|
||||||
"water.css": "^2.1.1",
|
|
||||||
"web-vitals": "^1.1.2"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"build": "bash ./build.sh"
|
||||||
"build": "react-scripts build",
|
|
||||||
"test": "react-scripts test",
|
|
||||||
"eject": "react-scripts eject"
|
|
||||||
},
|
|
||||||
"eslintConfig": {
|
|
||||||
"extends": [
|
|
||||||
"react-app",
|
|
||||||
"react-app/jest"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"browserslist": {
|
|
||||||
"production": [
|
|
||||||
">0.2%",
|
|
||||||
"not dead",
|
|
||||||
"not op_mini all"
|
|
||||||
],
|
|
||||||
"development": [
|
|
||||||
"last 1 chrome version",
|
|
||||||
"last 1 firefox version",
|
|
||||||
"last 1 safari version"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/react": "^17.0.34"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,36 +6,14 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="description" content="Personal music streaming platform" />
|
<meta name="description" content="Personal music streaming platform" />
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon.png" />
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon.png" />
|
||||||
<!--
|
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
|
||||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
|
||||||
-->
|
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
<!--
|
<link rel="stylesheet" href="%PUBLIC_URL%/msw-open-music.css" />
|
||||||
Notice the use of %PUBLIC_URL% in the tags above.
|
|
||||||
It will be replaced with the URL of the `public` folder during the build.
|
|
||||||
Only files inside the `public` folder can be referenced from the HTML.
|
|
||||||
|
|
||||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
|
||||||
work correctly both with client-side routing and a non-root public URL.
|
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
|
||||||
-->
|
|
||||||
<!-- Add to homescreen for Chrome on Android -->
|
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
<title>MSW Open Music</title>
|
<title>MSW Open Music</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<!--
|
<script type="module" src="%PUBLIC_URL%/msw-open-music.js"></script>
|
||||||
This HTML file is a template.
|
|
||||||
If you open it directly in the browser, you will see an empty page.
|
|
||||||
|
|
||||||
You can add webfonts, meta tags, or analytics to this file.
|
|
||||||
The build step will place the bundled scripts into the <body> tag.
|
|
||||||
|
|
||||||
To begin the development, run `npm start` or `yarn start`.
|
|
||||||
To create a production bundle, use `npm run build` or `yarn build`.
|
|
||||||
-->
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,11 +3,19 @@ html {
|
|||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
margin-top: 1rem;
|
padding-top: 1rem;
|
||||||
|
max-width: unset;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
#root {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.base {
|
.base {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-row-gap: 1em;
|
grid-row-gap: 1em;
|
||||||
|
max-width: 800px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
.header {
|
.header {
|
||||||
color: white;
|
color: white;
|
||||||
@@ -17,8 +25,11 @@ body {
|
|||||||
}
|
}
|
||||||
.title {
|
.title {
|
||||||
margin-left: 1em;
|
margin-left: 1em;
|
||||||
|
margin-right: 1em;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
.title-text {
|
.title-text {
|
||||||
margin-left: 1em;
|
margin-left: 1em;
|
||||||
@@ -92,3 +103,19 @@ dialog {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
import {
|
|
||||||
HashRouter as Router,
|
|
||||||
Routes,
|
|
||||||
Route,
|
|
||||||
NavLink,
|
|
||||||
} from "react-router-dom";
|
|
||||||
import "./App.css";
|
|
||||||
|
|
||||||
import GetRandomFiles from "./component/GetRandomFiles";
|
|
||||||
import SearchFiles from "./component/SearchFiles";
|
|
||||||
import SearchFolders from "./component/SearchFolders";
|
|
||||||
import FilesInFolder from "./component/FilesInFolder";
|
|
||||||
import Manage from "./component/Manage";
|
|
||||||
import Share from "./component/Share";
|
|
||||||
import AudioPlayer from "./component/AudioPlayer";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const [playingFile, setPlayingFile] = useState({});
|
|
||||||
return (
|
|
||||||
<div className="base">
|
|
||||||
<Router>
|
|
||||||
<header className="header">
|
|
||||||
<h3 className="title">
|
|
||||||
<img src="favicon.png" alt="logo" className="logo" />
|
|
||||||
<span className="title-text">MSW Open Music Project</span>
|
|
||||||
</h3>
|
|
||||||
<nav className="nav">
|
|
||||||
<NavLink to="/" className="nav-link">
|
|
||||||
Feeling luckly
|
|
||||||
</NavLink>
|
|
||||||
<NavLink to="/files" className="nav-link">
|
|
||||||
Files
|
|
||||||
</NavLink>
|
|
||||||
<NavLink to="/folders" className="nav-link">
|
|
||||||
Folders
|
|
||||||
</NavLink>
|
|
||||||
<NavLink to="/manage" className="nav-link">
|
|
||||||
Manage
|
|
||||||
</NavLink>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<Routes>
|
|
||||||
<Route
|
|
||||||
index
|
|
||||||
path="/"
|
|
||||||
element={<GetRandomFiles setPlayingFile={setPlayingFile} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/files"
|
|
||||||
element={<SearchFiles setPlayingFile={setPlayingFile} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/folders"
|
|
||||||
element={<SearchFolders setPlayingFile={setPlayingFile} />}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/folders/:id"
|
|
||||||
element={<FilesInFolder setPlayingFile={setPlayingFile} />}
|
|
||||||
/>
|
|
||||||
<Route path="/manage" element={<Manage />} />
|
|
||||||
<Route
|
|
||||||
path="/files/:id/share"
|
|
||||||
element={<Share setPlayingFile={setPlayingFile} />}
|
|
||||||
/>
|
|
||||||
</Routes>
|
|
||||||
</main>
|
|
||||||
<footer>
|
|
||||||
<AudioPlayer
|
|
||||||
playingFile={playingFile}
|
|
||||||
setPlayingFile={setPlayingFile}
|
|
||||||
/>
|
|
||||||
</footer>
|
|
||||||
</Router>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
177
web/src/App.jsx
Normal file
177
web/src/App.jsx
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { HashRouter as Router, Routes, Route, NavLink } from "react-router-dom";
|
||||||
|
import "./App.css";
|
||||||
|
|
||||||
|
import GetRandomFiles from "./component/GetRandomFiles";
|
||||||
|
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 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");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (playingFile.id === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const html = document.getElementsByTagName("html")[0];
|
||||||
|
const retStyle = html.style;
|
||||||
|
const bodyRetStyle = document.body.style
|
||||||
|
html.style = `
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
background-size: cover;
|
||||||
|
background-attachment: fixed;
|
||||||
|
background-position: center;
|
||||||
|
background-image: url("/api/v1/get_file_avatar?id=${playingFile.id}");
|
||||||
|
`;
|
||||||
|
document.body.style.opacity = 0.88;
|
||||||
|
return () => {
|
||||||
|
html.style = retStyle;
|
||||||
|
document.body.style = bodyRetStyle;
|
||||||
|
};
|
||||||
|
}, [playingFile.id]);
|
||||||
|
|
||||||
|
// 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">
|
||||||
|
{Tr("Feeling luckly")}
|
||||||
|
</NavLink>
|
||||||
|
<NavLink to="/files" className="nav-link">
|
||||||
|
{Tr("Files")}
|
||||||
|
</NavLink>
|
||||||
|
<NavLink to="/folders" className="nav-link">
|
||||||
|
{Tr("Folders")}
|
||||||
|
</NavLink>
|
||||||
|
<NavLink to="/manage" className="nav-link">
|
||||||
|
{Tr("Manage")}
|
||||||
|
</NavLink>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
index
|
||||||
|
path="/"
|
||||||
|
element={<GetRandomFiles setPlayingFile={setPlayingFile} />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/files"
|
||||||
|
element={<SearchFiles setPlayingFile={setPlayingFile} />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/folders"
|
||||||
|
element={<SearchFolders setPlayingFile={setPlayingFile} />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/folders/:id"
|
||||||
|
element={<FilesInFolder setPlayingFile={setPlayingFile} />}
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
<AudioPlayer
|
||||||
|
playingFile={playingFile}
|
||||||
|
setPlayingFile={setPlayingFile}
|
||||||
|
/>
|
||||||
|
</Router>
|
||||||
|
</langCodeContext.Provider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useNavigate } from "react-router";
|
|
||||||
import { CalcReadableFilesizeDetail } from "./Common";
|
|
||||||
import FfmpegConfig from "./FfmpegConfig";
|
|
||||||
import FileDialog from "./FileDialog";
|
|
||||||
|
|
||||||
function AudioPlayer(props) {
|
|
||||||
// props.playingFile
|
|
||||||
// props.setPlayingFile
|
|
||||||
|
|
||||||
const [fileDialogShowStatus, setFileDialogShowStatus] = useState(false);
|
|
||||||
const [loop, setLoop] = useState(true);
|
|
||||||
const [raw, setRaw] = useState(false);
|
|
||||||
const [prepare, setPrepare] = useState(false);
|
|
||||||
const [selectedFfmpegConfig, setSelectedFfmpegConfig] = useState({});
|
|
||||||
const [playingURL, setPlayingURL] = useState("");
|
|
||||||
const [isPreparing, setIsPreparing] = useState(false);
|
|
||||||
const [preparedFilesize, setPreparedFilesize] = useState(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// no playing file
|
|
||||||
if (props.playingFile.id === undefined) {
|
|
||||||
setPlayingURL("");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (raw) {
|
|
||||||
console.log("Play raw file");
|
|
||||||
setPlayingURL("/api/v1/get_file_direct?id=" + props.playingFile.id);
|
|
||||||
} else {
|
|
||||||
if (prepare) {
|
|
||||||
// prepare file
|
|
||||||
setIsPreparing(true);
|
|
||||||
fetch("/api/v1/prepare_file_stream_direct", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
id: props.playingFile.id,
|
|
||||||
config_name: selectedFfmpegConfig.name,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((data) => {
|
|
||||||
setPreparedFilesize(data.filesize);
|
|
||||||
setIsPreparing(false);
|
|
||||||
setPlayingURL(
|
|
||||||
`/api/v1/get_file_stream_direct?id=${props.playingFile.id}&config=${selectedFfmpegConfig.name}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setPlayingURL(
|
|
||||||
`/api/v1/get_file_stream?id=${props.playingFile.id}&config=${selectedFfmpegConfig.name}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [props.playingFile.id, raw, prepare, selectedFfmpegConfig]);
|
|
||||||
|
|
||||||
let navigate = useNavigate();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h5>Player status</h5>
|
|
||||||
{props.playingFile.id && (
|
|
||||||
<span>
|
|
||||||
<FileDialog
|
|
||||||
showStatus={fileDialogShowStatus}
|
|
||||||
setShowStatus={setFileDialogShowStatus}
|
|
||||||
file={props.playingFile}
|
|
||||||
setPlayingFile={() => {
|
|
||||||
return;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setFileDialogShowStatus(!fileDialogShowStatus);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.playingFile.filename}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() =>
|
|
||||||
navigate(`/folders/${props.playingFile.folder_id}`)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{props.playingFile.foldername}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button disabled>
|
|
||||||
{prepare
|
|
||||||
? CalcReadableFilesizeDetail(preparedFilesize)
|
|
||||||
: CalcReadableFilesizeDetail(props.playingFile.filesize)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{isPreparing && <button disabled>Preparing...</button>}
|
|
||||||
|
|
||||||
{playingURL !== "" && (
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
props.setPlayingFile({});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Stop
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
<input
|
|
||||||
checked={loop}
|
|
||||||
onChange={(event) => setLoop(event.target.checked)}
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
<label>Loop</label>
|
|
||||||
|
|
||||||
<input
|
|
||||||
checked={raw}
|
|
||||||
onChange={(event) => setRaw(event.target.checked)}
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
<label>Raw</label>
|
|
||||||
|
|
||||||
{!raw && (
|
|
||||||
<span>
|
|
||||||
<input
|
|
||||||
checked={prepare}
|
|
||||||
onChange={(event) => setPrepare(event.target.checked)}
|
|
||||||
type="checkbox"
|
|
||||||
/>
|
|
||||||
<label>Prepare</label>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{playingURL !== "" && (
|
|
||||||
<audio
|
|
||||||
controls
|
|
||||||
autoPlay
|
|
||||||
loop={loop}
|
|
||||||
className="audio-player"
|
|
||||||
src={playingURL}
|
|
||||||
></audio>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<FfmpegConfig
|
|
||||||
selectedFfmpegConfig={selectedFfmpegConfig}
|
|
||||||
setSelectedFfmpegConfig={setSelectedFfmpegConfig}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AudioPlayer;
|
|
||||||
275
web/src/component/AudioPlayer.jsx
Normal file
275
web/src/component/AudioPlayer.jsx
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
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
|
||||||
|
// props.setPlayingFile
|
||||||
|
|
||||||
|
const [fileDialogShowStatus, setFileDialogShowStatus] = useState(false);
|
||||||
|
const [loop, setLoop] = useState(true);
|
||||||
|
const [raw, setRaw] = useState(false);
|
||||||
|
const [prepare, setPrepare] = useState(false);
|
||||||
|
const [selectedFfmpegConfig, setSelectedFfmpegConfig] = useState({
|
||||||
|
name: "",
|
||||||
|
args: "",
|
||||||
|
});
|
||||||
|
const [playingURL, setPlayingURL] = useState("");
|
||||||
|
const [isPreparing, setIsPreparing] = useState(false);
|
||||||
|
const [timerCount, setTimerCount] = useState(0);
|
||||||
|
const [timerID, setTimerID] = useState(null);
|
||||||
|
const [beginPlayTime, setBeginPlayTime] = useState(null);
|
||||||
|
const [lastID, setLastID] = useState(null);
|
||||||
|
|
||||||
|
const recordPlaybackHistory = async (file_id, method) => {
|
||||||
|
if (file_id === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const player = document.getElementById('dom-player')
|
||||||
|
const endPlayTime = new Date()
|
||||||
|
let duration = parseInt((endPlayTime - beginPlayTime) / 1000)
|
||||||
|
const maxDuration = parseInt(player.duration)
|
||||||
|
// treat 85% of duration as finished
|
||||||
|
if (duration / maxDuration >= 0.85) {
|
||||||
|
method = 1
|
||||||
|
}
|
||||||
|
duration = duration < maxDuration ? duration : maxDuration
|
||||||
|
setBeginPlayTime(endPlayTime)
|
||||||
|
await fetch('/api/v1/record_playback', {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
playback: {
|
||||||
|
file_id,
|
||||||
|
method,
|
||||||
|
duration,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// init mediaSession API
|
||||||
|
useEffect(() => {
|
||||||
|
if (navigator.mediaSession) {
|
||||||
|
navigator.mediaSession.setActionHandler("stop", () => {
|
||||||
|
props.setPlayingFile({});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updatePlayMode = () => {
|
||||||
|
if (props.playingFile.id === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (raw) {
|
||||||
|
console.log("Play raw file");
|
||||||
|
setPlayingURL("/api/v1/get_file_direct?id=" + props.playingFile.id);
|
||||||
|
} else {
|
||||||
|
if (prepare) {
|
||||||
|
// prepare file
|
||||||
|
setIsPreparing(true);
|
||||||
|
fetch("/api/v1/prepare_file_stream_direct", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {"Content-Type": "application/json"},
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: props.playingFile.id,
|
||||||
|
config_name: selectedFfmpegConfig.name,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
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}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setPlayingURL(
|
||||||
|
`/api/v1/get_file_stream?id=${props.playingFile.id}&config=${selectedFfmpegConfig.name}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// media session related staff
|
||||||
|
if (navigator.mediaSession) {
|
||||||
|
navigator.mediaSession.metadata = new window.MediaMetadata({
|
||||||
|
title: props.playingFile.filename,
|
||||||
|
album: props.playingFile.foldername,
|
||||||
|
artwork: [{src: "/favicon.png", type: "image/png"}],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// no playing file
|
||||||
|
if (props.playingFile.id === undefined) {
|
||||||
|
// 3 music stopped
|
||||||
|
recordPlaybackHistory(lastID, 3)
|
||||||
|
setPlayingURL("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// crrently playing file, record interupt
|
||||||
|
if (playingURL) {
|
||||||
|
// 2 music changed
|
||||||
|
recordPlaybackHistory(lastID, 2)
|
||||||
|
}
|
||||||
|
setLastID(props.playingFile.id)
|
||||||
|
// have playingFile, record begin time
|
||||||
|
setBeginPlayTime(new Date())
|
||||||
|
updatePlayMode()
|
||||||
|
}, [props.playingFile.id]);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updatePlayMode()
|
||||||
|
}, [raw, prepare, selectedFfmpegConfig])
|
||||||
|
|
||||||
|
let navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer className="vertical">
|
||||||
|
<h5>{Tr("Player status")}</h5>
|
||||||
|
{props.playingFile.id && (
|
||||||
|
<span>
|
||||||
|
<FileDialog
|
||||||
|
showStatus={fileDialogShowStatus}
|
||||||
|
setShowStatus={setFileDialogShowStatus}
|
||||||
|
file={props.playingFile}
|
||||||
|
setPlayingFile={() => {
|
||||||
|
return;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setFileDialogShowStatus(!fileDialogShowStatus);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.playingFile.filename}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => navigate(`/folders/${props.playingFile.folder_id}`)}
|
||||||
|
>
|
||||||
|
{props.playingFile.foldername}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button disabled>
|
||||||
|
{CalcReadableFilesizeDetail(props.playingFile.filesize)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isPreparing && <button disabled>{Tr("Preparing...")}</button>}
|
||||||
|
|
||||||
|
{playingURL !== "" && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
props.setPlayingFile({});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Tr("Stop")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<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>{Tr("Loop")}</label>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
<input
|
||||||
|
checked={raw}
|
||||||
|
onChange={(event) => setRaw(event.target.checked)}
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<label>{Tr("Raw")}</label>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{!raw && (
|
||||||
|
<span>
|
||||||
|
<input
|
||||||
|
checked={prepare}
|
||||||
|
onChange={(event) => setPrepare(event.target.checked)}
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<label>{Tr("Prepare")}</label>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<audio
|
||||||
|
id="dom-player"
|
||||||
|
controls
|
||||||
|
autoPlay
|
||||||
|
className="audio-player"
|
||||||
|
src={playingURL}
|
||||||
|
onEnded={async () => {
|
||||||
|
const player = document.getElementById('dom-player')
|
||||||
|
if (loop) {
|
||||||
|
player.play()
|
||||||
|
}
|
||||||
|
// 1 music finished
|
||||||
|
recordPlaybackHistory(props.playingFile.id, 1)
|
||||||
|
}}
|
||||||
|
onPlay={async () => {
|
||||||
|
setBeginPlayTime(new Date());
|
||||||
|
}}
|
||||||
|
></audio>
|
||||||
|
|
||||||
|
<FfmpegConfig
|
||||||
|
selectedFfmpegConfig={selectedFfmpegConfig}
|
||||||
|
setSelectedFfmpegConfig={setSelectedFfmpegConfig}
|
||||||
|
/>
|
||||||
|
</footer >
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AudioPlayer;
|
||||||
@@ -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) {
|
export function CalcReadableFilesize(filesize) {
|
||||||
if (filesize < 1024) {
|
if (filesize < 1024) {
|
||||||
return filesize;
|
return filesize;
|
||||||
@@ -35,6 +43,30 @@ function numberWithCommas(x) {
|
|||||||
return 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() {
|
export function SayHello() {
|
||||||
return "Hello";
|
return "Hello";
|
||||||
}
|
}
|
||||||
|
|||||||
115
web/src/component/Database.jsx
Normal file
115
web/src/component/Database.jsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
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 webm"
|
||||||
|
);
|
||||||
|
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;
|
||||||
102
web/src/component/EditReview.jsx
Normal file
102
web/src/component/EditReview.jsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
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;
|
||||||
143
web/src/component/EditTag.jsx
Normal file
143
web/src/component/EditTag.jsx
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
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;
|
||||||
107
web/src/component/FeedbackPage.jsx
Normal file
107
web/src/component/FeedbackPage.jsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
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;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import * as React from 'react';
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
function FfmpegConfig(props) {
|
function FfmpegConfig(props) {
|
||||||
@@ -33,7 +34,7 @@ function FfmpegConfig(props) {
|
|||||||
<option key={ffmpegConfig.name}>{ffmpegConfig.name}</option>
|
<option key={ffmpegConfig.name}>{ffmpegConfig.name}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<span>{props.selectedFfmpegConfig.args}</span>
|
<span className="warp-word">{props.selectedFfmpegConfig.args}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import { useNavigate } from "react-router";
|
|
||||||
|
|
||||||
function FileDialog(props) {
|
|
||||||
// props.showStatus
|
|
||||||
// props.setShowStatus
|
|
||||||
// props.playingFile
|
|
||||||
// props.setPlayingFile
|
|
||||||
// props.file
|
|
||||||
|
|
||||||
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>
|
|
||||||
<a href={downloadURL} download>
|
|
||||||
<button>Download</button>
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
props.setPlayingFile(props.file);
|
|
||||||
props.setShowStatus(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Play
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
navigate(`/files/${props.file.id}/share`);
|
|
||||||
props.setShowStatus(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Share
|
|
||||||
</button>
|
|
||||||
<button onClick={() => props.setShowStatus(false)}>Close</button>
|
|
||||||
</dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FileDialog;
|
|
||||||
58
web/src/component/FileDialog.jsx
Normal file
58
web/src/component/FileDialog.jsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
import { Tr } from "../translate";
|
||||||
|
|
||||||
|
function FileDialog(props) {
|
||||||
|
// props.showStatus
|
||||||
|
// props.setShowStatus
|
||||||
|
// props.playingFile
|
||||||
|
// props.setPlayingFile
|
||||||
|
// props.file
|
||||||
|
|
||||||
|
let navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<dialog
|
||||||
|
open={props.showStatus}
|
||||||
|
style={{
|
||||||
|
zIndex: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
props.setPlayingFile(props.file);
|
||||||
|
props.setShowStatus(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.file.filename}
|
||||||
|
</p>
|
||||||
|
<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);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Tr("Play")}
|
||||||
|
</button>
|
||||||
|
<button onClick={() => props.setShowStatus(false)}>{Tr("Close")}</button>
|
||||||
|
</dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileDialog;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import * as React from 'react';
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { CalcReadableFilesize } from "./Common";
|
import { CalcReadableFilesize } from "./Common";
|
||||||
331
web/src/component/FileInfo.jsx
Normal file
331
web/src/component/FileInfo.jsx
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
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);
|
||||||
|
const [ffprobeInfo, setFfprobeInfo] = useState("");
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
<button onClick={async () => {
|
||||||
|
const resp = await fetch(`/api/v1/get_file_ffprobe_info`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: parseInt(params.id),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const text = await resp.text();
|
||||||
|
setFfprobeInfo(text);
|
||||||
|
}}>FFprobe</button>
|
||||||
|
|
||||||
|
{ffprobeInfo && <textarea
|
||||||
|
style={{
|
||||||
|
height: "30em",
|
||||||
|
}}
|
||||||
|
>{ffprobeInfo}</textarea>}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileInfo;
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import { useParams } from "react-router";
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import FilesTable from "./FilesTable";
|
|
||||||
|
|
||||||
function FilesInFolder(props) {
|
|
||||||
let params = useParams();
|
|
||||||
const [files, setFiles] = useState([]);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [offset, setOffset] = useState(0);
|
|
||||||
const limit = 10;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsLoading(true);
|
|
||||||
fetch("/api/v1/get_files_in_folder", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
folder_id: parseInt(params.id),
|
|
||||||
offset: offset,
|
|
||||||
limit: limit,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((data) => {
|
|
||||||
setFiles(data.files ? data.files : []);
|
|
||||||
})
|
|
||||||
.catch((error) => alert(error))
|
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false);
|
|
||||||
});
|
|
||||||
}, [params.id, offset]);
|
|
||||||
|
|
||||||
function nextPage() {
|
|
||||||
setOffset(offset + limit);
|
|
||||||
}
|
|
||||||
|
|
||||||
function lastPage() {
|
|
||||||
const offsetValue = offset - limit;
|
|
||||||
if (offsetValue < 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setOffset(offsetValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="page">
|
|
||||||
<h3>Files in Folder</h3>
|
|
||||||
<div className="search_toolbar">
|
|
||||||
<button onClick={lastPage}>Last page</button>
|
|
||||||
<button disabled>
|
|
||||||
{isLoading ? "Loading..." : `${offset} - ${offset + files.length}`}
|
|
||||||
</button>
|
|
||||||
<button onClick={nextPage}>Next page</button>
|
|
||||||
</div>
|
|
||||||
<FilesTable setPlayingFile={props.setPlayingFile} files={files} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FilesInFolder;
|
|
||||||
141
web/src/component/FilesInFolder.jsx
Normal file
141
web/src/component/FilesInFolder.jsx
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
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 = parseInt(query.get("o")) || 0;
|
||||||
|
const [newFoldername, setNewFoldername] = useState("");
|
||||||
|
const [folderPath, setFolderPath] = useState("");
|
||||||
|
const limit = 10;
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
setIsLoading(true);
|
||||||
|
fetch("/api/v1/get_files_in_folder", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {"Content-Type": "application/json"},
|
||||||
|
body: JSON.stringify({
|
||||||
|
folder_id: parseInt(params.id),
|
||||||
|
offset: offset,
|
||||||
|
limit: limit,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data.error) {
|
||||||
|
alert(data.error);
|
||||||
|
} else {
|
||||||
|
setFiles(data.files);
|
||||||
|
setFolderPath(data.folder);
|
||||||
|
if (data.files.length > 0) {
|
||||||
|
setNewFoldername(data.files[0].foldername);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => alert(error))
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refresh();
|
||||||
|
}, [params.id, offset]);
|
||||||
|
|
||||||
|
function nextPage() {
|
||||||
|
navigator(`/folders/${params.id}?o=${offset + limit}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function lastPage() {
|
||||||
|
const offsetValue = offset - limit;
|
||||||
|
if (offsetValue < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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>{Tr("Files in Folder")}</h3>
|
||||||
|
<div className="search_toolbar">
|
||||||
|
<button onClick={lastPage}>{Tr("Last page")}</button>
|
||||||
|
<button disabled>
|
||||||
|
{isLoading
|
||||||
|
? Tr("Loading...")
|
||||||
|
: `${offset} - ${offset + files.length}`}
|
||||||
|
</button>
|
||||||
|
<button onClick={nextPage}>{Tr("Next page")}</button>
|
||||||
|
</div>
|
||||||
|
<FilesTable setPlayingFile={props.setPlayingFile} files={files} />
|
||||||
|
<span>{folderPath}</span>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FilesInFolder;
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import * as React from 'react';
|
||||||
import FileEntry from "./FileEntry";
|
import FileEntry from "./FileEntry";
|
||||||
|
import { Tr } from "../translate";
|
||||||
|
|
||||||
function FilesTable(props) {
|
function FilesTable(props) {
|
||||||
if (props.files.length === 0) {
|
if (props.files.length === 0) {
|
||||||
@@ -8,9 +10,9 @@ function FilesTable(props) {
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Filename</th>
|
<th>{Tr("Filename")}</th>
|
||||||
<th>Folder Name</th>
|
<th>{Tr("Folder Name")}</th>
|
||||||
<th>Size</th>
|
<th>{Tr("Size")}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import * as React from 'react';
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
|
import { Tr } from "../translate";
|
||||||
|
|
||||||
function FoldersTable(props) {
|
function FoldersTable(props) {
|
||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
@@ -9,8 +11,8 @@ function FoldersTable(props) {
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Folder name</th>
|
<th>{Tr("Folder name")}</th>
|
||||||
<th>Action</th>
|
<th>{Tr("Action")}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -23,7 +25,7 @@ function FoldersTable(props) {
|
|||||||
{folder.foldername}
|
{folder.foldername}
|
||||||
</td>
|
</td>
|
||||||
<td onClick={() => navigate(`/folders/${folder.id}`)}>
|
<td onClick={() => navigate(`/folders/${folder.id}`)}>
|
||||||
<button>View</button>
|
<button>{Tr("View")}</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import FilesTable from "./FilesTable";
|
|
||||||
|
|
||||||
function GetRandomFiles(props) {
|
|
||||||
const [files, setFiles] = useState([]);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
function refresh(setFiles) {
|
|
||||||
setIsLoading(true);
|
|
||||||
fetch("/api/v1/get_random_files")
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((data) => {
|
|
||||||
setFiles(data.files);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
alert("get_random_files error: " + error);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
refresh(setFiles);
|
|
||||||
}, []);
|
|
||||||
return (
|
|
||||||
<div className="page">
|
|
||||||
<div className="search_toolbar">
|
|
||||||
<button className="refresh" onClick={() => refresh(setFiles)}>
|
|
||||||
{isLoading ? "Loading..." : "Refresh"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<FilesTable setPlayingFile={props.setPlayingFile} files={files} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default GetRandomFiles;
|
|
||||||
118
web/src/component/GetRandomFiles.jsx
Normal file
118
web/src/component/GetRandomFiles.jsx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import * as React 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);
|
||||||
|
|
||||||
|
const fetchRandomFiles = async () => {
|
||||||
|
const resp = await fetch("/api/v1/get_random_files");
|
||||||
|
const json = await resp.json();
|
||||||
|
return json.files;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getRandomFiles() {
|
||||||
|
setIsLoading(true);
|
||||||
|
fetchRandomFiles()
|
||||||
|
.then((data) => {
|
||||||
|
setFiles(data);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
alert("get_random_files error: " + error);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchRandomFilesWithTag = async (selectedTag) => {
|
||||||
|
const resp = await fetch("/api/v1/get_random_files_with_tag", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: parseInt(selectedTag),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const json = await resp.json();
|
||||||
|
return json.files;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getRandomFilesWithTag() {
|
||||||
|
setIsLoading(true);
|
||||||
|
fetchRandomFilesWithTag(selectedTag)
|
||||||
|
.then((files) => {
|
||||||
|
setFiles(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(() => {
|
||||||
|
getTags();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refresh();
|
||||||
|
}, [selectedTag]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page">
|
||||||
|
<div className="search_toolbar">
|
||||||
|
<button className="refresh" onClick={() => refresh(setFiles)}>
|
||||||
|
{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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GetRandomFiles;
|
||||||
75
web/src/component/Login.jsx
Normal file
75
web/src/component/Login.jsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
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,51 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
function Manage() {
|
|
||||||
const [token, setToken] = useState("");
|
|
||||||
const [walkPath, setWalkPath] = useState("");
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Update Database
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Manage;
|
|
||||||
101
web/src/component/Manage.jsx
Normal file
101
web/src/component/Manage.jsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
import Database from "./Database";
|
||||||
|
|
||||||
|
import { Tr, langCodeContext, LANG_OPTIONS } from "../translate";
|
||||||
|
import { useContext } from "react";
|
||||||
|
|
||||||
|
function Manage(props) {
|
||||||
|
let navigate = useNavigate();
|
||||||
|
const { langCode, setLangCode } = useContext(langCodeContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page">
|
||||||
|
<h2>{Tr("Manage")}</h2>
|
||||||
|
<p>
|
||||||
|
{Tr("Hi")}, {props.user.username}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<select
|
||||||
|
value={langCode}
|
||||||
|
onChange={(event) => {
|
||||||
|
setLangCode(event.target.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Object.keys(LANG_OPTIONS).map((code) => {
|
||||||
|
const langOption = LANG_OPTIONS[code];
|
||||||
|
return (
|
||||||
|
<option value={code} 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 />
|
||||||
|
<p>
|
||||||
|
<a
|
||||||
|
href="https://github.com/heimoshuiyu/msw-open-music"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{Tr("View source code on Github")}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Manage;
|
||||||
82
web/src/component/ManageUser.jsx
Normal file
82
web/src/component/ManageUser.jsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
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;
|
||||||
83
web/src/component/Register.jsx
Normal file
83
web/src/component/Register.jsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
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;
|
||||||
34
web/src/component/ReviewEntry.jsx
Normal file
34
web/src/component/ReviewEntry.jsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
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;
|
||||||
77
web/src/component/ReviewPage.jsx
Normal file
77
web/src/component/ReviewPage.jsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
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,26 @@
|
|||||||
import { useState, useEffect } from "react";
|
import * as React from 'react';
|
||||||
|
import { useState, useEffect, useContext } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useQuery } from "./Common";
|
||||||
import FilesTable from "./FilesTable";
|
import FilesTable from "./FilesTable";
|
||||||
|
import { Tr, tr, langCodeContext } from "../translate";
|
||||||
|
|
||||||
function SearchFiles(props) {
|
function SearchFiles(props) {
|
||||||
|
const navigator = useNavigate();
|
||||||
const [files, setFiles] = useState([]);
|
const [files, setFiles] = useState([]);
|
||||||
const [filename, setFilename] = useState("");
|
const query = useQuery();
|
||||||
const [offset, setOffset] = useState(0);
|
const filename = query.get("q") || "";
|
||||||
|
const [filenameInput, setFilenameInput] = useState(filename);
|
||||||
|
const offset = parseInt(query.get("o")) || 0;
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const limit = 10;
|
const limit = 10;
|
||||||
|
const { langCode } = useContext(langCodeContext);
|
||||||
|
|
||||||
function searchFiles() {
|
function searchFiles() {
|
||||||
|
// check empty filename
|
||||||
|
if (filename === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
fetch("/api/v1/search_files", {
|
fetch("/api/v1/search_files", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -33,7 +45,7 @@ function SearchFiles(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function nextPage() {
|
function nextPage() {
|
||||||
setOffset(offset + limit);
|
navigator(`/files?q=${filenameInput}&o=${offset + limit}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function lastPage() {
|
function lastPage() {
|
||||||
@@ -41,37 +53,38 @@ function SearchFiles(props) {
|
|||||||
if (offsetValue < 0) {
|
if (offsetValue < 0) {
|
||||||
return;
|
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 (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<h3>Search Files</h3>
|
<h3>{Tr("Search Files")}</h3>
|
||||||
<div className="search_toolbar">
|
<div className="search_toolbar">
|
||||||
<input
|
<input
|
||||||
onChange={(event) => setFilename(event.target.value)}
|
onChange={(event) => setFilenameInput(event.target.value)}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
searchFiles();
|
navigator(`/files?q=${filenameInput}&o=0`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Enter filename"
|
placeholder={tr("Enter filename", langCode)}
|
||||||
|
value={filenameInput}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
searchFiles();
|
navigator(`/files?q=${filenameInput}&o=0`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isLoading ? "Loading..." : "Search"}
|
{isLoading ? Tr("Loading...") : Tr("Search")}
|
||||||
</button>
|
</button>
|
||||||
<button onClick={lastPage}>Last page</button>
|
<button onClick={lastPage}>{Tr("Last page")}</button>
|
||||||
<button disabled>
|
<button disabled>
|
||||||
{offset} - {offset + files.length}
|
{offset} - {offset + files.length}
|
||||||
</button>
|
</button>
|
||||||
<button onClick={nextPage}>Next page</button>
|
<button onClick={nextPage}>{Tr("Next page")}</button>
|
||||||
</div>
|
</div>
|
||||||
<FilesTable setPlayingFile={props.setPlayingFile} files={files} />
|
<FilesTable setPlayingFile={props.setPlayingFile} files={files} />
|
||||||
</div>
|
</div>
|
||||||
@@ -1,12 +1,20 @@
|
|||||||
import { useEffect, useState } from "react";
|
import * as React from 'react';
|
||||||
|
import { useContext, useEffect, useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useQuery } from "./Common";
|
||||||
import FoldersTable from "./FoldersTable";
|
import FoldersTable from "./FoldersTable";
|
||||||
|
import { Tr, tr, langCodeContext } from "../translate";
|
||||||
|
|
||||||
function SearchFolders() {
|
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 [folders, setFolders] = useState([]);
|
||||||
const [offset, setOffset] = useState(0);
|
const offset = parseInt(query.get("o")) || 0;
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const limit = 10;
|
const limit = 10;
|
||||||
|
const { langCode } = useContext(langCodeContext);
|
||||||
|
|
||||||
function searchFolder() {
|
function searchFolder() {
|
||||||
if (foldername === "") {
|
if (foldername === "") {
|
||||||
@@ -35,7 +43,7 @@ function SearchFolders() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function nextPage() {
|
function nextPage() {
|
||||||
setOffset(offset + limit);
|
navigator(`/folders?q=${foldername}&o=${offset + limit}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function lastPage() {
|
function lastPage() {
|
||||||
@@ -43,33 +51,38 @@ function SearchFolders() {
|
|||||||
if (offsetValue < 0) {
|
if (offsetValue < 0) {
|
||||||
return;
|
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 (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<h3>Search Folders</h3>
|
<h3>{Tr("Search Folders")}</h3>
|
||||||
<div className="search_toolbar">
|
<div className="search_toolbar">
|
||||||
<input
|
<input
|
||||||
onChange={(event) => setFoldername(event.target.value)}
|
onChange={(event) => setFoldernameInput(event.target.value)}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
searchFolder();
|
navigator(`/folders?q=${foldernameInput}&o=0`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Enter folder name"
|
placeholder={tr("Enter folder name", langCode)}
|
||||||
|
value={foldernameInput}
|
||||||
/>
|
/>
|
||||||
<button onClick={searchFolder}>
|
<button
|
||||||
{isLoading ? "Loading..." : "Search"}
|
onClick={() => {
|
||||||
|
navigator(`/folders?q=${foldernameInput}&o=0`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isLoading ? Tr("Loading...") : Tr("Search")}
|
||||||
</button>
|
</button>
|
||||||
<button onClick={lastPage}>Last page</button>
|
<button onClick={lastPage}>{Tr("Last page")}</button>
|
||||||
<button disabled>
|
<button disabled>
|
||||||
{offset} - {offset + limit}
|
{offset} - {offset + limit}
|
||||||
</button>
|
</button>
|
||||||
<button onClick={nextPage}>Next page</button>
|
<button onClick={nextPage}>{Tr("Next page")}</button>
|
||||||
</div>
|
</div>
|
||||||
<FoldersTable folders={folders} />
|
<FoldersTable folders={folders} />
|
||||||
</div>
|
</div>
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useParams } from "react-router";
|
|
||||||
import FilesTable from "./FilesTable";
|
|
||||||
|
|
||||||
function Share(props) {
|
|
||||||
let params = useParams();
|
|
||||||
const [file, setFile] = useState([]);
|
|
||||||
useEffect(() => {
|
|
||||||
fetch("/api/v1/get_file_info", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
id: parseInt(params.id),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((data) => {
|
|
||||||
setFile([data]);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
alert("get_file_info error: " + error);
|
|
||||||
});
|
|
||||||
}, [params]);
|
|
||||||
return (
|
|
||||||
<div className="page">
|
|
||||||
<h3>Share with others!</h3>
|
|
||||||
<p>
|
|
||||||
👇 Click the filename below to enjoy music!
|
|
||||||
<br />
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Share link: <a href={window.location.href}>{window.location.href}</a>
|
|
||||||
</p>
|
|
||||||
<FilesTable setPlayingFile={props.setPlayingFile} files={file} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Share;
|
|
||||||
59
web/src/component/Share.jsx
Normal file
59
web/src/component/Share.jsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { useContext, useEffect, useState } from "react";
|
||||||
|
import { useParams } from "react-router";
|
||||||
|
import FilesTable from "./FilesTable";
|
||||||
|
import { Tr, tr, langCodeContext } from "../translate";
|
||||||
|
|
||||||
|
function Share(props) {
|
||||||
|
let params = useParams();
|
||||||
|
const { langCode } = useContext(langCodeContext);
|
||||||
|
|
||||||
|
const [file, setFile] = useState({});
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/api/v1/get_file_info", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: parseInt(params.id),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
setFile(data);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
alert("get_file_info error: " + error);
|
||||||
|
});
|
||||||
|
}, [params]);
|
||||||
|
|
||||||
|
// change title
|
||||||
|
useEffect(() => {
|
||||||
|
const oldTitle = document.title;
|
||||||
|
|
||||||
|
document.title = `${tr("Share", langCode)}🎵: ${
|
||||||
|
file.filename
|
||||||
|
} - MSW Open Music`;
|
||||||
|
|
||||||
|
// set title back
|
||||||
|
return () => {
|
||||||
|
document.title = oldTitle;
|
||||||
|
};
|
||||||
|
}, [file]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page">
|
||||||
|
<h3>{Tr("Share with others!")}</h3>
|
||||||
|
<p>
|
||||||
|
{Tr("Share link")}:{" "}
|
||||||
|
<a href={window.location.href}>{window.location.href}</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
👇 {Tr("Click the filename below to enjoy music!")}
|
||||||
|
<br />
|
||||||
|
</p>
|
||||||
|
<FilesTable setPlayingFile={props.setPlayingFile} files={[file]} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Share;
|
||||||
107
web/src/component/Tags.jsx
Normal file
107
web/src/component/Tags.jsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
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;
|
||||||
171
web/src/component/UserProfile.jsx
Normal file
171
web/src/component/UserProfile.jsx
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
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;
|
||||||
17
web/src/component/UserStatus.jsx
Normal file
17
web/src/component/UserStatus.jsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
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;
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import './index.css';
|
|
||||||
import 'water.css';
|
|
||||||
import App from './App';
|
|
||||||
import reportWebVitals from './reportWebVitals';
|
|
||||||
|
|
||||||
ReactDOM.render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<App />
|
|
||||||
</React.StrictMode>,
|
|
||||||
document.getElementById('root')
|
|
||||||
);
|
|
||||||
|
|
||||||
// If you want to start measuring performance in your app, pass a function
|
|
||||||
// to log results (for example: reportWebVitals(console.log))
|
|
||||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
|
||||||
reportWebVitals();
|
|
||||||
12
web/src/index.jsx
Normal file
12
web/src/index.jsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import './index.css';
|
||||||
|
import 'water.css';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
document.getElementById('root')
|
||||||
|
);
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
const reportWebVitals = onPerfEntry => {
|
|
||||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
|
||||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
|
||||||
getCLS(onPerfEntry);
|
|
||||||
getFID(onPerfEntry);
|
|
||||||
getFCP(onPerfEntry);
|
|
||||||
getLCP(onPerfEntry);
|
|
||||||
getTTFB(onPerfEntry);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default reportWebVitals;
|
|
||||||
45
web/src/translate/index.jsx
Normal file
45
web/src/translate/index.jsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { createContext } 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 };
|
||||||
107
web/src/translate/zh_CN.js
Normal file
107
web/src/translate/zh_CN.js
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
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": "文件夹内",
|
||||||
|
"preparing...": "转码中...",
|
||||||
|
"view source code on github": "在 Github 上查看源代码",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LANG_zh_CN;
|
||||||
Reference in New Issue
Block a user