Compare commits
2 Commits
v1.2.0
...
submit-web
| Author | SHA1 | Date | |
|---|---|---|---|
|
b6013ba1e3
|
|||
|
3457fde522
|
68
.github/workflows/build.yml
vendored
68
.github/workflows/build.yml
vendored
@@ -1,68 +0,0 @@
|
|||||||
# 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,10 +1,9 @@
|
|||||||
.PHONY: web linux windows
|
dist:
|
||||||
web:
|
|
||||||
cd web && npm install
|
cd web && npm install
|
||||||
cd web && npm run build
|
cd web && npm run build
|
||||||
|
|
||||||
linux:
|
linux:
|
||||||
go build -v -ldflags '-linkmode=external -extldflags=-static' -tags sqlite_omit_load_extension,netgo
|
go build
|
||||||
|
|
||||||
windows:
|
windows:
|
||||||
CC=x86_64-w64-mingw32-gcc CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -v
|
CC=x86_64-w64-mingw32-gcc CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build
|
||||||
|
|||||||
137
README-cn.md
137
README-cn.md
@@ -1,137 +0,0 @@
|
|||||||
# 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 和 数据库对象 的 数据结构。
|
|
||||||
447
README.md
447
README.md
@@ -1,139 +1,420 @@
|
|||||||
# 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
|
||||||
|
|
||||||
Screenshot
|
A light weight personal music streaming platform.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Features
|
[toc]
|
||||||
|
|
||||||
- 🔎 Index your existing music files, and record file name and folder information.
|
## TODO
|
||||||
|
|
||||||
- 📕 Use folder 📁 tag 🏷️ review 💬 to manage your music.
|
- Restructure,为多人协作做好准备
|
||||||
|
|
||||||
- 🌐 Provide a light weight web application with multi-language support.
|
### 前端部分更改
|
||||||
|
|
||||||
- 👥 Multi-user support.
|
- 修复页面 CSS 溢出问题
|
||||||
|
- 显示操作执行世界
|
||||||
|
|
||||||
- 🔥 Call `ffmpeg` with customizable preset to stream your music.
|
页面数量至少 10 个(目前 5 个),预计添加如下页面
|
||||||
|
|
||||||
- 🔗 Share music with others!
|
- 文件详情页,可以修改单个文件的信息
|
||||||
|
- 文件评论页,可对文件进行评论
|
||||||
|
- 最新动态页,查看最近播放的曲目、最近的评论
|
||||||
|
- 登录/注册页,取代现有的 token 逻辑
|
||||||
|
- FfmpegConfigs 配置页面
|
||||||
|
- 意见反馈的查看页面
|
||||||
|
|
||||||
### Try it if you...
|
### 后端部分更改
|
||||||
|
|
||||||
- Already saved a lot of music files on disk. 🖴
|
- 返回操作执行时间
|
||||||
|
|
||||||
- Downloaded tons of huge lossless music. 🎵
|
- 修复 Prepare 模式转码不完整但仍然被 tmpfs 记录为成功转码的问题
|
||||||
|
|
||||||
- Wants to stream your music files from PC/Server to PC/phone. 😋
|
- FfmpegConfigs 由目前的字典格式改为列表格式
|
||||||
|
- 为 sqlite3 添加数据库单线程锁
|
||||||
|
- 添加外键约束
|
||||||
|
- Update 功能自动检查重复的项目并忽略,只添加新的项目
|
||||||
|
- Token 验证方法改为 暱称 + 密码 的方法,管理员使用 admin 保留关键字作为暱称。
|
||||||
|
|
||||||
- Wants to share your stored music. 😘
|
需要 8 个 entities 和 6 个 relationship,目前有 3 个 entities,和 1 个 relationship
|
||||||
|
|
||||||
|
目前有
|
||||||
|
|
||||||
|
- files 文件表,数量 50,000
|
||||||
|
- folders 文件夹表,数量 3,000
|
||||||
|
- feedbacks 反馈留言表
|
||||||
|
|
||||||
|
计划添加
|
||||||
|
|
||||||
|
- users 用户表
|
||||||
|
- comments 管理表
|
||||||
|
- playbacks 播放记录表
|
||||||
|
- likes 点赞记录表
|
||||||
|
|
||||||
|
## 编译 & 构建
|
||||||
|
|
||||||
|
## How to build
|
||||||
|
|
||||||
|
### Build the back-end server
|
||||||
|
|
||||||
|
`make linux` or `make windows`
|
||||||
|
|
||||||
|
The executable file is named `msw-open-music` or `msw-open-music.exe`
|
||||||
|
|
||||||
|
### Build the font-end web pages
|
||||||
|
|
||||||
|
To build production web page `make web`
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
To start the development, run `cd web` and `npm start`
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
1. Modify the `secret` in `config.json`
|
Start back-end server. Server will listen on 8080 port.
|
||||||
|
|
||||||
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.
|
Build the font-end web page, then go to <http://127.0.0.1:8080>
|
||||||
|
|
||||||
The front-end HTML files are under `web/build`
|
By default:
|
||||||
|
|
||||||
### Setup first admin account
|
- URL matched `/api/*` will process by back-end server.
|
||||||
|
- Others URL matched `/*` will be served files under `web/build/`
|
||||||
|
|
||||||
The first administrator account will be active automatically, other administrator accounts need active manually.
|
### Run back-end server
|
||||||
|
|
||||||
Go to register page, select the role to admin, and register the first admin account.
|
Configuration file is `config.json`, **Please modify your `token`** 。
|
||||||
|
|
||||||
#### config.json
|
Default `ffmpeg_threads` is 1. Seems value larger than 1 will not increase the audio encode speed.
|
||||||
|
|
||||||
- `secret` string type. Secret to encrypt the session.
|
#### config.json description
|
||||||
|
|
||||||
- `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.** Directory will be created if not exists.
|
- `root` string type. Directory to store temporary files. Default is `/tmp`, **please modify this directory if you are using Windows.**
|
||||||
- `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.
|
|
||||||
|
|
||||||
For windows user, make sure you have `ffmpeg` installed.
|
### Run font-end web page
|
||||||
|
|
||||||
## Development
|
Open your web browser to <http://127.0.0.1:8080> you will see the web pages.
|
||||||
|
|
||||||
Any issues or pull requests are welcome.
|
## About tmpfs
|
||||||
|
|
||||||
### Major changes log
|
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.
|
||||||
|
|
||||||
- `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.
|
||||||
|
|
||||||
### Back-end API design
|
## Change log
|
||||||
|
|
||||||
|
- `v1.0.0` First version. Ready to use in production environment.
|
||||||
|
- `v1.1.0` Use `React` to rewrite the font-end web pages (Previous using `Vue`).
|
||||||
|
|
||||||
|
## Back-end API references
|
||||||
|
|
||||||
|
API named `stream` means it transfer data using `io.Copy`, which **DO NOT** support continue getting a partially-downloaded audio.
|
||||||
|
|
||||||
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"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Sometime errors happen, server will return the following JSON object, which `error` is the detailed error message.
|
### Anonymous API
|
||||||
|
|
||||||
```json
|
Anonymous API can be called by anonymous.
|
||||||
{
|
|
||||||
"error": "Wrong password"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
API does not need to send any data should use `GET` method, otherwise use `POST` method.
|
- `/api/v1/hello` Just for test purpose.
|
||||||
|
|
||||||
Server use cookies to authenticate a user. Any request without cookies will be consider from an anonymous user (aka. user with ID `1`).
|
- `/api/v1/get_file` Get a file with `stream` mode.
|
||||||
|
|
||||||
Some important source code files:
|
- Request example
|
||||||
|
|
||||||
- `pkg/api/api.go` define URL
|
```json
|
||||||
|
{
|
||||||
|
"id": 123
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
- `pkg/database/sql_stmt.go` define SQL queries and do the init job.
|
- `/api/v1/get_file_direct` Get a file with standart `http` methods, implement by `http.ServeFile` method.
|
||||||
|
|
||||||
- `pkg/database/struct.go` define JSON structures for database entities.
|
- Request example
|
||||||
|
|
||||||
|
`/api/v1/get_file_direct?id=30`
|
||||||
|
|
||||||
|
- `/api/v1/search_files` Search files by filename.
|
||||||
|
|
||||||
|
- Request example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"filename": "miku",
|
||||||
|
"limit": 10,
|
||||||
|
"offset" 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Search all files' name like `%miku%`. `%` is the wildcard in SQL. For example, `"filename": "miku%hatsune"` can match `hatsune miku`.
|
||||||
|
|
||||||
|
`limit` Numbers of files in the respond. Should be within 1 - 10;
|
||||||
|
|
||||||
|
`offset` It is the offset of the result, related to the page turning function.
|
||||||
|
|
||||||
|
- Respond example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"id": 30,
|
||||||
|
"folder_id": 100,
|
||||||
|
"folder_name": "wonderful",
|
||||||
|
"filename": "memories.flac",
|
||||||
|
"filesize": 1048576
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 31,
|
||||||
|
"folder_id": 100,
|
||||||
|
"folder_name": "wonderful",
|
||||||
|
"filename": "memories (instrunment).flac",
|
||||||
|
"filesize": 1248531
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`id` Identification of file.
|
||||||
|
|
||||||
|
`folder_id` Identification of folder.
|
||||||
|
|
||||||
|
`foldername` Folder name where the file in.
|
||||||
|
|
||||||
|
`filename` File name.
|
||||||
|
|
||||||
|
`filesize` File size, unit is byte.
|
||||||
|
|
||||||
|
- `/api/v1/search_folders` Search folders.
|
||||||
|
|
||||||
|
- Request example.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"foldername": "miku",
|
||||||
|
"limit": 10,
|
||||||
|
"offset": 0,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Search all folders' name like `%miku%`. `%` is the wildcard in SQL. For example, `"filename": "miku%hatsune"` can match `hatsune miku`.
|
||||||
|
|
||||||
|
`limit` Numbers of files in the respond. Should be within 1 - 10;
|
||||||
|
|
||||||
|
`offset` It is the offset of the result, related to the page turning function.
|
||||||
|
|
||||||
|
- Respond example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"id": 100,
|
||||||
|
"foldername": "folder name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 101,
|
||||||
|
"foldername": "folder name (another)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`id` Identification of folder.
|
||||||
|
|
||||||
|
`foldername` Folder name.
|
||||||
|
|
||||||
|
- `/api/v1/get_files_in_folder` Get files in a specify folder.
|
||||||
|
|
||||||
|
- Request example.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"folder_id": 123,
|
||||||
|
"limit": 10,
|
||||||
|
"offset": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Respond example.
|
||||||
|
|
||||||
|
Same with `/api/v1/search_files`
|
||||||
|
|
||||||
|
- `/api/v1/get_random_files` Randomly get 10 files.
|
||||||
|
|
||||||
|
- Request example.
|
||||||
|
|
||||||
|
GET `/api/v1/get_random_files`
|
||||||
|
|
||||||
|
- Respond example.
|
||||||
|
|
||||||
|
Same with `/api/v1/search_files`
|
||||||
|
|
||||||
|
- `/api/v1/get_file_stream`
|
||||||
|
|
||||||
|
Stream file with a ffmpeg config name.
|
||||||
|
|
||||||
|
- Request example.
|
||||||
|
|
||||||
|
GET `/api/v1/get_file_stream?id=123&config=OPUS%20128k`
|
||||||
|
|
||||||
|
- `/api/v1/get_ffmpeg_config_list`
|
||||||
|
|
||||||
|
Get ffmpeg config list
|
||||||
|
|
||||||
|
- Request example
|
||||||
|
|
||||||
|
GET `/api/v1/get_ffmpeg_config_list`
|
||||||
|
|
||||||
|
- Respond example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ffmpeg_config_list": [
|
||||||
|
{"name": "OPUS 256k", "args": "-c:a libopus -ab 256k"},
|
||||||
|
{"name": "WAV", "args": "-c:a wav"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `/api/v1/feedback` Send a feedback.
|
||||||
|
|
||||||
|
- Request example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"feedback": "some suggestions..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Respond OK.
|
||||||
|
|
||||||
|
- `/api/v1/get_file_info` Get information of a specify file.
|
||||||
|
|
||||||
|
- Request example.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ID": 123
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Respond example.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 30,
|
||||||
|
"folder_id": 100,
|
||||||
|
"folder_name": "wonderful",
|
||||||
|
"filename": "memories.flac",
|
||||||
|
"filesize": 1048576
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
- `/api/v1/get_file_stream_direct` Get a ffmpeg converted file with native http method. This API support continue getting a partially-downloaded audio. Note, you should call `/api/v1/prepare_file_stream_direct` first and wait for its respond, then call this API.
|
||||||
|
|
||||||
|
- Request example
|
||||||
|
|
||||||
|
GET `/api/v1/get_file_stream_direct?id=123&config=OPUS%20128k`
|
||||||
|
|
||||||
|
- `/api/v1/prepare_file_stream_direct` Ask server to convert a file with specific ffmpeg config name. When the conver process is finished, server will reply with the converted file size.
|
||||||
|
|
||||||
|
- Request example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 123,
|
||||||
|
"config_name": "OPUS 128k"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Respond example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"filesize": 1973241
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### API needs token
|
||||||
|
|
||||||
|
- `/api/v1/walk` Walk directory, add all files and folders to database.
|
||||||
|
|
||||||
|
- Request example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "your token",
|
||||||
|
"root": "/path/to/root",
|
||||||
|
"pattern": [".wav", ".flac"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`token` The token in `config.json` file.
|
||||||
|
|
||||||
|
`root` Root directory server will walk throught
|
||||||
|
|
||||||
|
`pattern` A list of pattern that files ends with. Only files matched a pattern in list will be add to database.
|
||||||
|
|
||||||
|
- Respond OK
|
||||||
|
|
||||||
|
- `/api/v1/reset` Rest the **files and folders table**
|
||||||
|
|
||||||
|
- Request example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "your token"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Respond OK
|
||||||
|
|
||||||
|
- `/api/v1/add_ffmpeg_config` Add ffmpeg config.
|
||||||
|
|
||||||
|
Will be changed in future.
|
||||||
|
|
||||||
|
- Request example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "your token",
|
||||||
|
"name": "OPUS",
|
||||||
|
"ffmpeg_config": {
|
||||||
|
"args": "-c:a libopus -ab 256k"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`name` Name of the ffmpeg config.
|
||||||
|
|
||||||
|
`ffmpeg_config`
|
||||||
|
|
||||||
|
`args`
|
||||||
|
|
||||||
|
- Respond OK
|
||||||
|
|
||||||
|
## Font-end API references
|
||||||
|
|
||||||
|
Currently only few APIs in font-end.
|
||||||
|
|
||||||
|
- `/#/share/39`
|
||||||
|
|
||||||
|
Share a specific file.
|
||||||
|
|
||||||
|
- `/#/search-folders/2614`
|
||||||
|
|
||||||
|
Show files in a specific folder.
|
||||||
|
|||||||
99
config.json
99
config.json
@@ -1,80 +1,23 @@
|
|||||||
{
|
{
|
||||||
"api": {
|
"api": {
|
||||||
"secret": "CHANGE_YOUR_SECRET_HERE",
|
"database_name": "music.sqlite3",
|
||||||
"database_name": "music.sqlite3",
|
"addr": ":8080",
|
||||||
"single_thread": true,
|
"token": "!! config your very strong token here !!",
|
||||||
"addr": ":8080",
|
"ffmpeg_threads": 1,
|
||||||
"ffmpeg_threads": 1,
|
"ffmpeg_config_list": [
|
||||||
"ffmpeg_config_list": [
|
{"name": "OPUS 128k", "args": "-c:a libopus -ab 128k"},
|
||||||
{
|
{"name": "OPUS 96k", "args": "-c:a libopus -ab 96k"},
|
||||||
"name": "WEBM OPUS 128k",
|
{"name": "OPUS 256k", "args": "-c:a libopus -ab 256k"},
|
||||||
"args": "-c:a libopus -ab 128k -vn",
|
{"name": "OPUS 320k", "args": "-c:a libopus -ab 320k"},
|
||||||
"format": "webm"
|
{"name": "OPUS 512k", "args": "-c:a libopus -ab 512k"},
|
||||||
},
|
{"name": "AAC 128k", "args": "-c:a aac -ab 128k"},
|
||||||
{
|
{"name": "AAC 256k", "args": "-c:a aac -ab 256k"},
|
||||||
"name": "WEBM OPUS 96k",
|
{"name": "全损音质 32k", "args": "-c:a libopus -ab 32k"}
|
||||||
"args": "-c:a libopus -ab 96k -vn",
|
]
|
||||||
"format": "webm"
|
},
|
||||||
},
|
"tmpfs": {
|
||||||
{
|
"file_life_time": 600,
|
||||||
"name": "WEBM OPUS 256k",
|
"cleaner_internal": 1,
|
||||||
"args": "-c:a libopus -ab 256k -vn",
|
"root": "/tmp/"
|
||||||
"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/"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
36
docs/problem_description.md
Normal file
36
docs/problem_description.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# DBMS Group Project Problem Description
|
||||||
|
|
||||||
|
- Group 1
|
||||||
|
|
||||||
|
The Internet infrastructure construction has made the network speed development faster. With the fast Internet, people are gradually migrating various data and services to the cloud. For example, NetEase Cloud Music, Spotify, and Apple Music, we call them streaming media platforms. The definition of streaming media platform is that users purchase the digital copyright of music and then play the music online on the platform.
|
||||||
|
|
||||||
|
Generally speaking, users cannot buy music that is not available on the platform. The user cannot download the digital file of the music (the user purchases the right to play instead of the right to copy). Users cannot upload their music to the platform.
|
||||||
|
|
||||||
|
However, in the era of digital copyright, there are still many advantages to getting original music files, such as no need to install a dedicated player; free copying to other devices (without violating copyright); no risk of music unavailable from the platform; no play records and privacy will be tracked by the platform.
|
||||||
|
|
||||||
|
Some people don't like streaming platforms. They like to collect music (download or buy CDs) and save it on their computers. But as more and more music is collected (over 70,000 songs and in total size of 800GB), it becomes very difficult to manage files. It is difficult for them to find where the songs they want to listen to are saved. Also, lossless music files are large and difficult to play online.
|
||||||
|
|
||||||
|
As long as there no such "Self-hosted music streaming platform" software available, we decided to develop a project based on database knowledge to help people who have collected a lot of music to enjoy their music simply.
|
||||||
|
|
||||||
|
We will handle various relevant types of data in our database. Including song name, album name, file size, update date, rating, comment, user information, etc. They are highly relevant, so using a relational database will be a good choice.
|
||||||
|
|
||||||
|
The features of the project we designed are as follows:
|
||||||
|
|
||||||
|
- Open. Independent front-end (GUI) and back-end (server program), using API to communicate.
|
||||||
|
- Easy to use. Minimize dependencies, allowing users to configure quickly and simply.
|
||||||
|
- Lightweight. The program is small in size and quick to install.
|
||||||
|
- High performance. Only do what should be done, no features that will lead to poor performance.
|
||||||
|
- Cross-platform. The project can run on computers, mobile phones, Linux, Windows, macOS, and X86 and ARM processor architectures.
|
||||||
|
- Extensibility. Access to cloud OSS (Object Storage Service), reverse proxy, or other external software.
|
||||||
|
|
||||||
|
Our project has the following functions:
|
||||||
|
|
||||||
|
- Index file. Index local files into the database.
|
||||||
|
- Search. Search for music based on name/album/tag/comment, sorted by rating or other columns.
|
||||||
|
- Play. Play music online, play music randomly and play music at a low bit rate on a bad network.
|
||||||
|
- User management. Users can register and log in.
|
||||||
|
- Comment. Users can give a like or comment on the music.
|
||||||
|
- Management. The administrator can upload music, update or delete the database.
|
||||||
|
- Share. Generate a link to share the music with others.
|
||||||
|
|
||||||
|
After research and discussion, in order to meet the above requirements, we decided to use the Golang programming language on the backend. SQLite as a database program. Vue as the front-end GUI interface.
|
||||||
BIN
erdiagram.png
BIN
erdiagram.png
Binary file not shown.
|
Before Width: | Height: | Size: 93 KiB |
10
go.mod
10
go.mod
@@ -1,11 +1,5 @@
|
|||||||
module msw-open-music
|
module msw-open-music
|
||||||
|
|
||||||
go 1.18
|
go 1.16
|
||||||
|
|
||||||
require (
|
require github.com/mattn/go-sqlite3 v1.14.7 // indirect
|
||||||
github.com/gorilla/sessions v1.2.1
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.14
|
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
|
|
||||||
)
|
|
||||||
|
|
||||||
require github.com/gorilla/securecookie v1.1.1 // indirect
|
|
||||||
|
|||||||
10
go.sum
10
go.sum
@@ -1,8 +1,2 @@
|
|||||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
github.com/mattn/go-sqlite3 v1.14.7 h1:fxWBnXkxfM6sRiuH3bqJ4CfzZojMOLVc0UTsTglEghA=
|
||||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
|
||||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.14 h1:qZgc/Rwetq+MtyE18WhzjokPD93dNqLGNT3QJuLvBGw=
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
|
|
||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
|
||||||
|
|||||||
750
internal/pkg/api/api.go
Normal file
750
internal/pkg/api/api.go
Normal file
@@ -0,0 +1,750 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"msw-open-music/internal/pkg/database"
|
||||||
|
"msw-open-music/internal/pkg/tmpfs"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type API struct {
|
||||||
|
Db *database.Database
|
||||||
|
Server http.Server
|
||||||
|
token string
|
||||||
|
APIConfig APIConfig
|
||||||
|
Tmpfs *tmpfs.Tmpfs
|
||||||
|
}
|
||||||
|
|
||||||
|
type FfmpegConfigList struct {
|
||||||
|
FfmpegConfigList []FfmpegConfig `json:"ffmpeg_config_list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddFfmpegConfigRequest struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
FfmpegConfig FfmpegConfig `json:"ffmpeg_config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FfmpegConfig struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Args string `json:"args"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Status struct {
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
|
}
|
||||||
|
var ok Status = Status{
|
||||||
|
Status: "OK",
|
||||||
|
}
|
||||||
|
|
||||||
|
type WalkRequest struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
Root string `json:"root"`
|
||||||
|
Pattern []string `json:"pattern"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResetRequest struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchFilesRequest struct {
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
Limit int64 `json:"limit"`
|
||||||
|
Offset int64 `json:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchFoldersRequest struct {
|
||||||
|
Foldername string `json:"foldername"`
|
||||||
|
Limit int64 `json:"limit"`
|
||||||
|
Offset int64 `json:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchFilesResponse struct {
|
||||||
|
Files []database.File `json:"files"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchFoldersResponse struct {
|
||||||
|
Folders []database.Folder `json:"folders"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetFilesInFolderRequest struct {
|
||||||
|
Folder_id int64 `json:"folder_id"`
|
||||||
|
Limit int64 `json:"limit"`
|
||||||
|
Offset int64 `json:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetFilesInFolderResponse struct {
|
||||||
|
Files *[]database.File `json:"files"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetRandomFilesResponse struct {
|
||||||
|
Files *[]database.File `json:"files"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) HandleGetRandomFiles(w http.ResponseWriter, r *http.Request) {
|
||||||
|
files, err := api.Db.GetRandomFiles(10);
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
getRandomFilesResponse := &GetRandomFilesResponse{
|
||||||
|
Files: &files,
|
||||||
|
}
|
||||||
|
log.Println("[api] Get random files")
|
||||||
|
json.NewEncoder(w).Encode(getRandomFilesResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) HandleGetFilesInFolder(w http.ResponseWriter, r *http.Request) {
|
||||||
|
getFilesInFolderRequest := &GetFilesInFolderRequest{
|
||||||
|
Folder_id: -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.NewDecoder(r.Body).Decode(getFilesInFolderRequest)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check empyt
|
||||||
|
if getFilesInFolderRequest.Folder_id < 0 {
|
||||||
|
api.HandleErrorString(w, r, `"folder_id" can't be none or negative`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
files, err := api.Db.GetFilesInFolder(getFilesInFolderRequest.Folder_id, getFilesInFolderRequest.Limit, getFilesInFolderRequest.Offset)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getFilesInFolderResponse := &GetFilesInFolderResponse{
|
||||||
|
Files: &files,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("[api] Get files in folder", getFilesInFolderRequest.Folder_id)
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(getFilesInFolderResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) HandleError(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
|
api.HandleErrorString(w, r, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) HandleErrorCode(w http.ResponseWriter, r *http.Request, err error, code int) {
|
||||||
|
api.HandleErrorStringCode(w, r, err.Error(), code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) HandleErrorString(w http.ResponseWriter, r *http.Request, errorString string) {
|
||||||
|
api.HandleErrorStringCode(w, r, errorString, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) HandleErrorStringCode(w http.ResponseWriter, r *http.Request, errorString string, code int) {
|
||||||
|
log.Println("[api] [Error]", code, errorString)
|
||||||
|
errStatus := &Status{
|
||||||
|
Status: errorString,
|
||||||
|
}
|
||||||
|
w.WriteHeader(code)
|
||||||
|
json.NewEncoder(w).Encode(errStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) HandleReset(w http.ResponseWriter, r *http.Request) {
|
||||||
|
resetRequest := &ResetRequest{}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(resetRequest)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check token
|
||||||
|
err = api.CheckToken(w, r, resetRequest.Token)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset
|
||||||
|
err = api.Db.ResetFiles()
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = api.Db.ResetFolder()
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.HandleStatus(w, r, "Database reseted")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) HandleWalk(w http.ResponseWriter, r *http.Request) {
|
||||||
|
walkRequest := &WalkRequest{}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(walkRequest)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check token match
|
||||||
|
err = api.CheckToken(w, r, walkRequest.Token)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check root empty
|
||||||
|
if walkRequest.Root == "" {
|
||||||
|
api.HandleErrorString(w, r, `key "root" can't be empty`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check pattern empty
|
||||||
|
if len(walkRequest.Pattern) == 0 {
|
||||||
|
api.HandleErrorString(w, r, `"[]pattern" can't be empty`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// walk
|
||||||
|
err = api.Db.Walk(walkRequest.Root, walkRequest.Pattern)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.HandleStatus(w, r, "Database udpated")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) HandleOK(w http.ResponseWriter, r *http.Request) {
|
||||||
|
json.NewEncoder(w).Encode(&ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) HandleStatus(w http.ResponseWriter, r *http.Request, status string) {
|
||||||
|
s := &Status{
|
||||||
|
Status: status,
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) HandleSearchFiles(w http.ResponseWriter, r *http.Request) {
|
||||||
|
searchFilesRequest := &SearchFilesRequest{}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(searchFilesRequest)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check empty
|
||||||
|
if searchFilesRequest.Filename == "" {
|
||||||
|
api.HandleErrorString(w, r, `"filename" can't be empty`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if api.CheckLimit(w, r, searchFilesRequest.Limit) != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
searchFilesResponse := &SearchFilesResponse{}
|
||||||
|
|
||||||
|
searchFilesResponse.Files, err = api.Db.SearchFiles(searchFilesRequest.Filename, searchFilesRequest.Limit, searchFilesRequest.Offset)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("[api] Search files", searchFilesRequest.Filename, searchFilesRequest.Limit, searchFilesRequest.Offset)
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(searchFilesResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) CheckLimit(w http.ResponseWriter, r *http.Request, limit int64) (error) {
|
||||||
|
if limit <= 0 || limit > 10 {
|
||||||
|
log.Println("[api] [Warning] Limit error", limit)
|
||||||
|
err := errors.New(`"limit" can't be zero or more than 10`)
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) HandleSearchFolders(w http.ResponseWriter, r *http.Request) {
|
||||||
|
searchFoldersRequest := &SearchFoldersRequest{}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(searchFoldersRequest)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check empty
|
||||||
|
if searchFoldersRequest.Foldername == "" {
|
||||||
|
api.HandleErrorString(w, r, `"foldername" can't be empty`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if api.CheckLimit(w, r, searchFoldersRequest.Limit) != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
searchFoldersResponse := &SearchFoldersResponse{}
|
||||||
|
|
||||||
|
searchFoldersResponse.Folders, err = api.Db.SearchFolders(searchFoldersRequest.Foldername, searchFoldersRequest.Limit, searchFoldersRequest.Offset)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("[api] Search folders", searchFoldersRequest.Foldername, searchFoldersRequest.Limit, searchFoldersRequest.Offset)
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(searchFoldersResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetFileRequest struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) HandleGetFileInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
|
getFileRequest := &GetFileRequest{
|
||||||
|
ID: -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.NewDecoder(r.Body).Decode(getFileRequest)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check empty
|
||||||
|
if getFileRequest.ID < 0 {
|
||||||
|
api.HandleErrorString(w, r, `"id" can't be none or negative`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := api.Db.GetFile(getFileRequest.ID)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.NewEncoder(w).Encode(file)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) CheckGetFileStream(w http.ResponseWriter, r *http.Request) (error) {
|
||||||
|
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 err
|
||||||
|
}
|
||||||
|
_, err = strconv.Atoi(ids[0])
|
||||||
|
if err != nil {
|
||||||
|
err = errors.New(`parameter "id" should be an integer`)
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
configs := q["config"]
|
||||||
|
if len(configs) == 0 {
|
||||||
|
err = errors.New(`parameter "config" can't be empty`)
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) GetFfmpegConfig(configName string) (FfmpegConfig, bool) {
|
||||||
|
ffmpegConfig := FfmpegConfig{}
|
||||||
|
for _, f := range api.APIConfig.FfmpegConfigList {
|
||||||
|
if f.Name == configName {
|
||||||
|
ffmpegConfig = f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ffmpegConfig.Name == "" {
|
||||||
|
return ffmpegConfig, false
|
||||||
|
}
|
||||||
|
return ffmpegConfig, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) HandleGetFileStream(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := api.CheckGetFileStream(w, r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
q := r.URL.Query()
|
||||||
|
ids := q["id"]
|
||||||
|
id, err := strconv.Atoi(ids[0])
|
||||||
|
configs := q["config"]
|
||||||
|
configName := configs[0]
|
||||||
|
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] Stream file", path, configName)
|
||||||
|
|
||||||
|
ffmpegConfig, ok := api.GetFfmpegConfig(configName)
|
||||||
|
if !ok {
|
||||||
|
api.HandleErrorStringCode(w, r, `ffmpeg config not found`, 404)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
args := strings.Split(ffmpegConfig.Args, " ")
|
||||||
|
startArgs := []string {"-threads", strconv.FormatInt(api.APIConfig.FfmpegThreads, 10), "-i", path}
|
||||||
|
endArgs := []string {"-vn", "-f", "ogg", "-"}
|
||||||
|
ffmpegArgs := append(startArgs, args...)
|
||||||
|
ffmpegArgs = append(ffmpegArgs, endArgs...)
|
||||||
|
cmd := exec.Command("ffmpeg", ffmpegArgs...)
|
||||||
|
cmd.Stdout = w
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type PrepareFileStreamDirectRequest struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
ConfigName string `json:"config_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PrepareFileStreamDirectResponse struct {
|
||||||
|
Filesize int64 `json:"filesize"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) HandlePrepareFileStreamDirect(w http.ResponseWriter, r *http.Request) {
|
||||||
|
prepareFileStreamDirectRequst := &PrepareFileStreamDirectRequest{
|
||||||
|
ID: -1,
|
||||||
|
}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(prepareFileStreamDirectRequst)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check empty
|
||||||
|
if prepareFileStreamDirectRequst.ID < 0 {
|
||||||
|
api.HandleErrorString(w, r, `"id" can't be none or negative`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if prepareFileStreamDirectRequst.ConfigName == "" {
|
||||||
|
api.HandleErrorString(w, r, `"config_name" can't be empty`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := api.Db.GetFile(prepareFileStreamDirectRequst.ID)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
srcPath, err := file.Path()
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("[api] Prepare stream direct file", srcPath, prepareFileStreamDirectRequst.ConfigName)
|
||||||
|
ffmpegConfig, ok := api.GetFfmpegConfig(prepareFileStreamDirectRequst.ConfigName)
|
||||||
|
if !ok {
|
||||||
|
api.HandleErrorStringCode(w, r, `ffmpeg config not found`, 404)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
objPath := api.Tmpfs.GetObjFilePath(prepareFileStreamDirectRequst.ID, prepareFileStreamDirectRequst.ConfigName)
|
||||||
|
|
||||||
|
// check obj file exists
|
||||||
|
exists := api.Tmpfs.Exits(objPath)
|
||||||
|
if exists {
|
||||||
|
fileInfo, err := os.Stat(objPath)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
prepareFileStreamDirectResponse := &PrepareFileStreamDirectResponse{
|
||||||
|
Filesize: fileInfo.Size(),
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(prepareFileStreamDirectResponse)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.Tmpfs.Record(objPath)
|
||||||
|
args := strings.Split(ffmpegConfig.Args, " ")
|
||||||
|
startArgs := []string {"-threads", strconv.FormatInt(api.APIConfig.FfmpegThreads, 10), "-i", srcPath}
|
||||||
|
endArgs := []string {"-vn", "-y", objPath}
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
prepareFileStreamDirectResponse := &PrepareFileStreamDirectResponse{
|
||||||
|
Filesize: fileInfo.Size(),
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(prepareFileStreamDirectResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) HandleGetFileStreamDirect(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := api.CheckGetFileStream(w, r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
q := r.URL.Query()
|
||||||
|
ids := q["id"]
|
||||||
|
id, err := strconv.Atoi(ids[0])
|
||||||
|
configs := q["config"]
|
||||||
|
configName := configs[0]
|
||||||
|
|
||||||
|
path := api.Tmpfs.GetObjFilePath(int64(id), configName)
|
||||||
|
if api.Tmpfs.Exits(path) {
|
||||||
|
api.Tmpfs.Record(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("[api] Get direct cached file", path)
|
||||||
|
|
||||||
|
http.ServeFile(w, r, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) HandleGetFileDirect(w http.ResponseWriter, r *http.Request) {
|
||||||
|
q := r.URL.Query()
|
||||||
|
ids := q["id"]
|
||||||
|
if len(ids) == 0 {
|
||||||
|
api.HandleErrorString(w, r, `parameter "id" can't be empty`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := strconv.Atoi(ids[0])
|
||||||
|
if err != nil {
|
||||||
|
api.HandleErrorString(w, r, `parameter "id" should be an integer`)
|
||||||
|
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 direct raw file", path)
|
||||||
|
|
||||||
|
http.ServeFile(w, r, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) HandleGetFile(w http.ResponseWriter, r *http.Request) {
|
||||||
|
getFileRequest := &GetFileRequest{
|
||||||
|
ID: -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.NewDecoder(r.Body).Decode(getFileRequest)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check empty
|
||||||
|
if getFileRequest.ID < 0 {
|
||||||
|
api.HandleErrorString(w, r, `"id" can't be none or negative`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := api.Db.GetFile(getFileRequest.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 pipe raw file", path)
|
||||||
|
|
||||||
|
src, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer src.Close()
|
||||||
|
io.Copy(w, src)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) HandleGetFfmpegConfigs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Println("[api] Get ffmpeg config list")
|
||||||
|
ffmpegConfigList:= &FfmpegConfigList{
|
||||||
|
FfmpegConfigList: api.APIConfig.FfmpegConfigList,
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(&ffmpegConfigList)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeedbackRequest struct {
|
||||||
|
Feedback string `json:"feedback"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) HandleFeedback(w http.ResponseWriter, r *http.Request) {
|
||||||
|
feedbackRequest := &FeedbackRequest{}
|
||||||
|
err :=json.NewDecoder(r.Body).Decode(feedbackRequest)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check empty feedback
|
||||||
|
if feedbackRequest.Feedback == "" {
|
||||||
|
api.HandleErrorString(w, r, `"feedback" can't be empty`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("[api] Feedback", feedbackRequest.Feedback)
|
||||||
|
|
||||||
|
headerBuff := &bytes.Buffer{}
|
||||||
|
err = r.Header.Write(headerBuff)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
header := headerBuff.String()
|
||||||
|
|
||||||
|
err = api.Db.InsertFeedback(time.Now().Unix(), feedbackRequest.Feedback, header)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
api.HandleOK(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAPIConfig() (APIConfig) {
|
||||||
|
apiConfig := APIConfig{}
|
||||||
|
return apiConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIConfig struct {
|
||||||
|
DatabaseName string `json:"database_name"`
|
||||||
|
Addr string `json:"addr"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
FfmpegThreads int64 `json:"ffmpeg_threads"`
|
||||||
|
FfmpegConfigList []FfmpegConfig `json:"ffmpeg_config_list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
APIConfig APIConfig `json:"api"`
|
||||||
|
TmpfsConfig tmpfs.TmpfsConfig `json:"tmpfs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAPI(config Config) (*API, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
apiConfig := config.APIConfig
|
||||||
|
tmpfsConfig := config.TmpfsConfig
|
||||||
|
|
||||||
|
db, err := database.NewDatabase(apiConfig.DatabaseName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
apiMux := http.NewServeMux()
|
||||||
|
|
||||||
|
api := &API{
|
||||||
|
Db: db,
|
||||||
|
Server: http.Server{
|
||||||
|
Addr: apiConfig.Addr,
|
||||||
|
Handler: mux,
|
||||||
|
},
|
||||||
|
APIConfig: apiConfig,
|
||||||
|
}
|
||||||
|
api.Tmpfs = tmpfs.NewTmpfs(tmpfsConfig)
|
||||||
|
|
||||||
|
// mount api
|
||||||
|
apiMux.HandleFunc("/hello", api.HandleOK)
|
||||||
|
apiMux.HandleFunc("/get_file", api.HandleGetFile)
|
||||||
|
apiMux.HandleFunc("/get_file_direct", api.HandleGetFileDirect)
|
||||||
|
apiMux.HandleFunc("/search_files", api.HandleSearchFiles)
|
||||||
|
apiMux.HandleFunc("/search_folders", api.HandleSearchFolders)
|
||||||
|
apiMux.HandleFunc("/get_files_in_folder", api.HandleGetFilesInFolder)
|
||||||
|
apiMux.HandleFunc("/get_random_files", api.HandleGetRandomFiles)
|
||||||
|
apiMux.HandleFunc("/get_file_stream", api.HandleGetFileStream)
|
||||||
|
apiMux.HandleFunc("/get_ffmpeg_config_list", api.HandleGetFfmpegConfigs)
|
||||||
|
apiMux.HandleFunc("/feedback", api.HandleFeedback)
|
||||||
|
apiMux.HandleFunc("/get_file_info", api.HandleGetFileInfo)
|
||||||
|
apiMux.HandleFunc("/get_file_stream_direct", api.HandleGetFileStreamDirect)
|
||||||
|
apiMux.HandleFunc("/prepare_file_stream_direct", api.HandlePrepareFileStreamDirect)
|
||||||
|
// below needs token
|
||||||
|
apiMux.HandleFunc("/walk", api.HandleWalk)
|
||||||
|
apiMux.HandleFunc("/reset", api.HandleReset)
|
||||||
|
apiMux.HandleFunc("/add_ffmpeg_config", api.HandleAddFfmpegConfig)
|
||||||
|
|
||||||
|
mux.Handle("/api/v1/", http.StripPrefix("/api/v1", apiMux))
|
||||||
|
mux.Handle("/", http.StripPrefix("/", http.FileServer(http.Dir("web/build"))))
|
||||||
|
|
||||||
|
api.token = apiConfig.Token
|
||||||
|
|
||||||
|
return api, nil
|
||||||
|
}
|
||||||
441
internal/pkg/database/database.go
Normal file
441
internal/pkg/database/database.go
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var initFilesTableQuery = `CREATE TABLE IF NOT EXISTS files (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
folder_id INTEGER NOT NULL,
|
||||||
|
filename TEXT NOT NULL,
|
||||||
|
filesize INTEGER NOT NULL
|
||||||
|
);`
|
||||||
|
var initFoldersTableQuery = `CREATE TABLE IF NOT EXISTS folders (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
folder TEXT NOT NULL,
|
||||||
|
foldername TEXT NOT NULL
|
||||||
|
);`
|
||||||
|
var initFeedbacksTableQuery = `CREATE TABLE IF NOT EXISTS feedbacks (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
time INTEGER NOT NULL,
|
||||||
|
feedback TEXT NOT NULL,
|
||||||
|
header TEXT NOT NULL
|
||||||
|
);`
|
||||||
|
var insertFolderQuery = `INSERT INTO folders (folder, foldername) VALUES (?, ?);`
|
||||||
|
var findFolderQuery = `SELECT id FROM folders WHERE folder = ? LIMIT 1;`
|
||||||
|
var insertFileQuery = `INSERT INTO files (folder_id, filename, filesize) VALUES (?, ?, ?);`
|
||||||
|
var searchFilesQuery = `SELECT files.id, files.folder_id, files.filename, folders.foldername, files.filesize FROM files JOIN folders ON files.folder_id = folders.id WHERE filename LIKE ? LIMIT ? OFFSET ?;`
|
||||||
|
var getFolderQuery = `SELECT folder FROM folders WHERE id = ? LIMIT 1;`
|
||||||
|
var dropFilesQuery = `DROP TABLE files;`
|
||||||
|
var dropFolderQuery = `DROP TABLE folders;`
|
||||||
|
var getFileQuery = `SELECT files.id, files.folder_id, files.filename, folders.foldername, files.filesize FROM files JOIN folders ON files.folder_id = folders.id WHERE files.id = ? LIMIT 1;`
|
||||||
|
var searchFoldersQuery = `SELECT id, folder, foldername FROM folders WHERE foldername LIKE ? LIMIT ? OFFSET ?;`
|
||||||
|
var getFilesInFolderQuery = `SELECT files.id, files.filename, files.filesize, folders.foldername FROM files JOIN folders ON files.folder_id = folders.id WHERE folder_id = ? LIMIT ? OFFSET ?;`
|
||||||
|
var getRandomFilesQuery = `SELECT files.id, files.folder_id, files.filename, folders.foldername, files.filesize FROM files JOIN folders on files.folder_id = folders.id ORDER BY RANDOM() LIMIT ?;`
|
||||||
|
var insertFeedbackQuery = `INSERT INTO feedbacks (time, feedback, header) VALUES (?, ?, ?);`
|
||||||
|
|
||||||
|
type Database struct {
|
||||||
|
sqlConn *sql.DB
|
||||||
|
stmt *Stmt
|
||||||
|
}
|
||||||
|
|
||||||
|
type Stmt struct {
|
||||||
|
initFilesTable *sql.Stmt
|
||||||
|
initFoldersTable *sql.Stmt
|
||||||
|
initFeedbacksTable *sql.Stmt
|
||||||
|
insertFolder *sql.Stmt
|
||||||
|
insertFile *sql.Stmt
|
||||||
|
findFolder *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
|
||||||
|
insertFeedback *sql.Stmt
|
||||||
|
}
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
Db *Database `json:"-"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Folder_id int64 `json:"folder_id"`
|
||||||
|
Foldername string `json:"foldername"`
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
Filesize int64 `json:"filesize"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Folder struct {
|
||||||
|
Db *Database `json:"-"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Folder string `json:"-"`
|
||||||
|
Foldername string `json:"foldername"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
rows, err := database.stmt.getRandomFiles.Query(limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
files := make([]File, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
file := File{
|
||||||
|
Db: database,
|
||||||
|
}
|
||||||
|
err = rows.Scan(&file.ID, &file.Folder_id, &file.Filename, &file.Foldername, &file.Filesize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
files = append(files, file)
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (database *Database) GetFilesInFolder(folder_id int64, limit int64, offset int64) ([]File, error) {
|
||||||
|
rows, err := database.stmt.getFilesInFolder.Query(folder_id, limit, offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
files := make([]File, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
file := File{
|
||||||
|
Db: database,
|
||||||
|
Folder_id: folder_id,
|
||||||
|
}
|
||||||
|
err = rows.Scan(&file.ID, &file.Filename, &file.Filesize, &file.Foldername)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
files = append(files, file)
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (database *Database) SearchFolders(foldername string, limit int64, offset int64) ([]Folder, error) {
|
||||||
|
rows, err := database.stmt.searchFolders.Query("%"+foldername+"%", limit, offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("Error searching folders at query " + err.Error())
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
folders := make([]Folder, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
folder := Folder{
|
||||||
|
Db: database,
|
||||||
|
}
|
||||||
|
err = rows.Scan(&folder.ID, &folder.Folder, &folder.Foldername)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("Error scanning SearchFolders" + err.Error())
|
||||||
|
}
|
||||||
|
folders = append(folders, folder)
|
||||||
|
}
|
||||||
|
return folders, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (database *Database) GetFile(id int64) (*File, error) {
|
||||||
|
file := &File{
|
||||||
|
Db: database,
|
||||||
|
}
|
||||||
|
err := database.stmt.getFile.QueryRow(id).Scan(&file.ID, &file.Folder_id, &file.Filename, &file.Foldername, &file.Filesize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return file, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (database *Database) ResetFiles() (error) {
|
||||||
|
log.Println("[db] Reset files")
|
||||||
|
var err error
|
||||||
|
_, err = database.stmt.dropFiles.Exec()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = database.stmt.initFilesTable.Exec()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (database *Database) ResetFolder() (error) {
|
||||||
|
log.Println("[db] Reset folders")
|
||||||
|
var err error
|
||||||
|
_, err = database.stmt.dropFolder.Exec()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = database.stmt.initFoldersTable.Exec()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (database *Database) Walk(root string, pattern []string) (error) {
|
||||||
|
patternDict := make(map[string]bool)
|
||||||
|
for _, v := range pattern {
|
||||||
|
patternDict[v] = true
|
||||||
|
}
|
||||||
|
log.Println("[db] Walk", root, patternDict)
|
||||||
|
return filepath.Walk(root, func (path string, info os.FileInfo, err error) (error) {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// check pattern
|
||||||
|
ext := filepath.Ext(info.Name())
|
||||||
|
if _, ok := patternDict[ext]; !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert file, folder will aut created
|
||||||
|
err = database.Insert(path, info.Size())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Path() (string, error) {
|
||||||
|
folder, err := f.Db.GetFolder(f.Folder_id)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return filepath.Join(folder.Folder, f.Filename), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (database *Database) GetFolder(folderId int64) (*Folder, error) {
|
||||||
|
folder := &Folder{
|
||||||
|
Db: database,
|
||||||
|
}
|
||||||
|
err := database.stmt.getFolder.QueryRow(folderId).Scan(&folder.Folder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return folder, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (database *Database) SearchFiles(filename string, limit int64, offset int64) ([]File, error) {
|
||||||
|
rows, err := database.stmt.searchFiles.Query("%"+filename+"%", limit, offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("Error searching files at query " + err.Error())
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
files := make([]File, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var file File = File{
|
||||||
|
Db: database,
|
||||||
|
}
|
||||||
|
err = rows.Scan(&file.ID, &file.Folder_id, &file.Filename, &file.Foldername, &file.Filesize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("Error scanning SearchFiles " + err.Error())
|
||||||
|
}
|
||||||
|
files = append(files, file)
|
||||||
|
}
|
||||||
|
if err = rows.Err(); err != nil {
|
||||||
|
return nil, errors.New("Error scanning SearchFiles exit without full result" + err.Error())
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (database *Database) FindFolder(folder string) (int64, error) {
|
||||||
|
var id int64
|
||||||
|
err := database.stmt.findFolder.QueryRow(folder).Scan(&id)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (database *Database) InsertFolder(folder string) (int64, error) {
|
||||||
|
result, err := database.stmt.insertFolder.Exec(folder, filepath.Base(folder))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
lastInsertId, err := result.LastInsertId()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return lastInsertId, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (database *Database) InsertFile(folderId int64, filename string, filesize int64) (error) {
|
||||||
|
_, err := database.stmt.insertFile.Exec(folderId, filename, filesize)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (database *Database) Insert(path string, filesize int64) (error) {
|
||||||
|
folder, filename := filepath.Split(path)
|
||||||
|
folderId, err := database.FindFolder(folder)
|
||||||
|
if err != nil {
|
||||||
|
folderId, err = database.InsertFolder(folder)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = database.InsertFile(folderId, filename, filesize)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
stmt := &Stmt{}
|
||||||
|
|
||||||
|
// init files table
|
||||||
|
stmt.initFilesTable, err = sqlConn.Prepare(initFilesTableQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// init folders table
|
||||||
|
stmt.initFoldersTable, err = sqlConn.Prepare(initFoldersTableQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// init feedbacks tables
|
||||||
|
stmt.initFeedbacksTable, err = sqlConn.Prepare(initFeedbacksTableQuery)
|
||||||
|
if err != nil {
|
||||||
|
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()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// init insert folder statement
|
||||||
|
stmt.insertFolder, err = sqlConn.Prepare(insertFolderQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// init findFolder statement
|
||||||
|
stmt.findFolder, err = sqlConn.Prepare(findFolderQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// init insertFile stmt
|
||||||
|
stmt.insertFile, err = sqlConn.Prepare(insertFileQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// init searchFile stmt
|
||||||
|
stmt.searchFiles, err = sqlConn.Prepare(searchFilesQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// init getFolder stmt
|
||||||
|
stmt.getFolder, err = sqlConn.Prepare(getFolderQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// init dropFolder stmt
|
||||||
|
stmt.dropFolder, err = sqlConn.Prepare(dropFolderQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// init dropFiles stmt
|
||||||
|
stmt.dropFiles, err = sqlConn.Prepare(dropFilesQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// init getFile stmt
|
||||||
|
stmt.getFile, err = sqlConn.Prepare(getFileQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// init searchFolder stmt
|
||||||
|
stmt.searchFolders, err = sqlConn.Prepare(searchFoldersQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// init getFilesInFolder stmt
|
||||||
|
stmt.getFilesInFolder, err = sqlConn.Prepare(getFilesInFolderQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// init getRandomFiles
|
||||||
|
stmt.getRandomFiles, err = sqlConn.Prepare(getRandomFilesQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// init insertFeedback
|
||||||
|
stmt.insertFeedback, err = sqlConn.Prepare(insertFeedbackQuery)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return stmt, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDatabase(dbName string) (*Database, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// open database
|
||||||
|
sqlConn, err := sql.Open("sqlite3", dbName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare statement
|
||||||
|
stmt, err := NewPreparedStatement(sqlConn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// new database
|
||||||
|
database := &Database{
|
||||||
|
sqlConn: sqlConn,
|
||||||
|
stmt: stmt,
|
||||||
|
}
|
||||||
|
|
||||||
|
return database, nil
|
||||||
|
}
|
||||||
69
internal/pkg/tmpfs/tmpfs.go
Normal file
69
internal/pkg/tmpfs/tmpfs.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package tmpfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Tmpfs struct {
|
||||||
|
record map[string]int64
|
||||||
|
Config TmpfsConfig
|
||||||
|
wg sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tmpfs *Tmpfs) GetObjFilePath(id int64, configName string) (string) {
|
||||||
|
return filepath.Join(tmpfs.Config.Root, strconv.FormatInt(id, 10) + "." + configName + ".ogg")
|
||||||
|
}
|
||||||
|
|
||||||
|
type TmpfsConfig struct {
|
||||||
|
FileLifeTime int64 `json:"file_life_time"`
|
||||||
|
CleanerInternal int64 `json:"cleaner_internal"`
|
||||||
|
Root string `json:"root"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTmpfsConfig() (*TmpfsConfig) {
|
||||||
|
config := &TmpfsConfig{}
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTmpfs(config TmpfsConfig) *Tmpfs {
|
||||||
|
tmpfs := &Tmpfs{
|
||||||
|
record: make(map[string]int64),
|
||||||
|
Config: config,
|
||||||
|
}
|
||||||
|
tmpfs.wg.Add(1)
|
||||||
|
go tmpfs.Cleaner()
|
||||||
|
return tmpfs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tmpfs *Tmpfs) Record(filename string) {
|
||||||
|
tmpfs.record[filename] = time.Now().Unix()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tmpfs *Tmpfs) Exits(filename string) (bool) {
|
||||||
|
_, ok := tmpfs.record[filename]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tmpfs *Tmpfs) Cleaner() {
|
||||||
|
var err error
|
||||||
|
for {
|
||||||
|
now := time.Now().Unix()
|
||||||
|
for key, value := range tmpfs.record {
|
||||||
|
if now - value > tmpfs.Config.FileLifeTime {
|
||||||
|
err = os.Remove(key)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("[tmpfs] Failed to remove file", err)
|
||||||
|
}
|
||||||
|
log.Println("[tmpfs] Deleted file", key)
|
||||||
|
delete(tmpfs.record, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
7
main.go
7
main.go
@@ -4,8 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"log"
|
"log"
|
||||||
"msw-open-music/pkg/api"
|
"msw-open-music/internal/pkg/api"
|
||||||
"msw-open-music/pkg/commonconfig"
|
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,11 +14,12 @@ 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 := commonconfig.Config{}
|
config := api.Config{}
|
||||||
configFile, err := os.Open(ConfigFilePath)
|
configFile, err := os.Open(ConfigFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
@@ -37,6 +37,7 @@ 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())
|
||||||
}
|
}
|
||||||
|
|||||||
105
pkg/api/api.go
105
pkg/api/api.go
@@ -1,105 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gorilla/sessions"
|
|
||||||
"msw-open-music/pkg/commonconfig"
|
|
||||||
"msw-open-music/pkg/database"
|
|
||||||
"msw-open-music/pkg/tmpfs"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type API struct {
|
|
||||||
Db *database.Database
|
|
||||||
Server http.Server
|
|
||||||
APIConfig commonconfig.APIConfig
|
|
||||||
Tmpfs *tmpfs.Tmpfs
|
|
||||||
store *sessions.CookieStore
|
|
||||||
defaultSessionName string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAPI(config commonconfig.Config) (*API, error) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
apiConfig := config.APIConfig
|
|
||||||
tmpfsConfig := config.TmpfsConfig
|
|
||||||
|
|
||||||
db, err := database.NewDatabase(apiConfig.DatabaseName, apiConfig.SingleThread)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
store := sessions.NewCookieStore([]byte(config.APIConfig.SECRET))
|
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
apiMux := http.NewServeMux()
|
|
||||||
|
|
||||||
api := &API{
|
|
||||||
Db: db,
|
|
||||||
Server: http.Server{
|
|
||||||
Addr: apiConfig.Addr,
|
|
||||||
Handler: mux,
|
|
||||||
},
|
|
||||||
APIConfig: apiConfig,
|
|
||||||
store: store,
|
|
||||||
defaultSessionName: "msw-open-music",
|
|
||||||
}
|
|
||||||
api.Tmpfs = tmpfs.NewTmpfs(tmpfsConfig)
|
|
||||||
|
|
||||||
// mount api
|
|
||||||
apiMux.HandleFunc("/hello", api.HandleOK)
|
|
||||||
apiMux.HandleFunc("/get_file", api.HandleGetFile)
|
|
||||||
apiMux.HandleFunc("/get_file_direct", api.HandleGetFileDirect)
|
|
||||||
apiMux.HandleFunc("/search_files", api.HandleSearchFiles)
|
|
||||||
apiMux.HandleFunc("/search_folders", api.HandleSearchFolders)
|
|
||||||
apiMux.HandleFunc("/get_files_in_folder", api.HandleGetFilesInFolder)
|
|
||||||
apiMux.HandleFunc("/get_random_files", api.HandleGetRandomFiles)
|
|
||||||
apiMux.HandleFunc("/get_random_files_with_tag", api.HandleGetRandomFilesWithTag)
|
|
||||||
apiMux.HandleFunc("/get_file_stream", api.HandleGetFileStream)
|
|
||||||
apiMux.HandleFunc("/get_ffmpeg_config_list", api.HandleGetFfmpegConfigs)
|
|
||||||
apiMux.HandleFunc("/get_file_info", api.HandleGetFileInfo)
|
|
||||||
apiMux.HandleFunc("/get_file_stream_direct", api.HandleGetFileStreamDirect)
|
|
||||||
apiMux.HandleFunc("/prepare_file_stream_direct", api.HandlePrepareFileStreamDirect)
|
|
||||||
apiMux.HandleFunc("/delete_file", api.HandleDeleteFile)
|
|
||||||
apiMux.HandleFunc("/update_filename", api.HandleUpdateFilename)
|
|
||||||
apiMux.HandleFunc("/reset_filename", api.HandleResetFilename)
|
|
||||||
apiMux.HandleFunc("/reset_foldername", api.HandleResetFoldername)
|
|
||||||
// feedback
|
|
||||||
apiMux.HandleFunc("/feedback", api.HandleFeedback)
|
|
||||||
apiMux.HandleFunc("/get_feedbacks", api.HandleGetFeedbacks)
|
|
||||||
apiMux.HandleFunc("/delete_feedback", api.HandleDeleteFeedback)
|
|
||||||
// user
|
|
||||||
apiMux.HandleFunc("/login", api.HandleLogin)
|
|
||||||
apiMux.HandleFunc("/register", api.HandleRegister)
|
|
||||||
apiMux.HandleFunc("/logout", api.LoginAsAnonymous)
|
|
||||||
apiMux.HandleFunc("/get_user_info", api.HandleGetUserInfo)
|
|
||||||
apiMux.HandleFunc("/get_users", api.HandleGetUsers)
|
|
||||||
apiMux.HandleFunc("/update_user_active", api.HandleUpdateUserActive)
|
|
||||||
apiMux.HandleFunc("/update_username", api.HandleUpdateUsername)
|
|
||||||
apiMux.HandleFunc("/update_user_password", api.HandleUpdateUserPassword)
|
|
||||||
// tag
|
|
||||||
apiMux.HandleFunc("/get_tags", api.HandleGetTags)
|
|
||||||
apiMux.HandleFunc("/get_tag_info", api.HandleGetTagInfo)
|
|
||||||
apiMux.HandleFunc("/insert_tag", api.HandleInsertTag)
|
|
||||||
apiMux.HandleFunc("/update_tag", api.HandleUpdateTag)
|
|
||||||
apiMux.HandleFunc("/put_tag_on_file", api.HandlePutTagOnFile)
|
|
||||||
apiMux.HandleFunc("/get_tags_on_file", api.HandleGetTagsOnFile)
|
|
||||||
apiMux.HandleFunc("/delete_tag_on_file", api.HandleDeleteTagOnFile)
|
|
||||||
apiMux.HandleFunc("/delete_tag", api.HandleDeleteTag)
|
|
||||||
// folder
|
|
||||||
apiMux.HandleFunc("/update_foldername", api.HandleUpdateFoldername)
|
|
||||||
// review
|
|
||||||
apiMux.HandleFunc("/insert_review", api.HandleInsertReview)
|
|
||||||
apiMux.HandleFunc("/get_reviews_on_file", api.HandleGetReviewsOnFile)
|
|
||||||
apiMux.HandleFunc("/get_review", api.HandleGetReview)
|
|
||||||
apiMux.HandleFunc("/update_review", api.HandleUpdateReview)
|
|
||||||
apiMux.HandleFunc("/delete_review", api.HandleDeleteReview)
|
|
||||||
apiMux.HandleFunc("/get_reviews_by_user", api.HandleGetReviewsByUser)
|
|
||||||
// database
|
|
||||||
apiMux.HandleFunc("/walk", api.HandleWalk)
|
|
||||||
apiMux.HandleFunc("/reset", api.HandleReset)
|
|
||||||
|
|
||||||
mux.Handle("/api/v1/", http.StripPrefix("/api/v1", api.PermissionMiddleware(apiMux)))
|
|
||||||
mux.Handle("/", http.StripPrefix("/", http.FileServer(http.Dir("web/build"))))
|
|
||||||
|
|
||||||
return api, nil
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (api *API) CheckLimit(w http.ResponseWriter, r *http.Request, limit int64) error {
|
|
||||||
if limit <= 0 || limit > 10 {
|
|
||||||
log.Println("[api] [Warning] Limit error", limit)
|
|
||||||
err := errors.New(`"limit" can't be zero or more than 10`)
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Status struct {
|
|
||||||
Status string `json:"status,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) HandleStatus(w http.ResponseWriter, r *http.Request, status string) {
|
|
||||||
s := &Status{
|
|
||||||
Status: status,
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
var ok Status = Status{
|
|
||||||
Status: "OK",
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) HandleOK(w http.ResponseWriter, r *http.Request) {
|
|
||||||
json.NewEncoder(w).Encode(&ok)
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type WalkRequest struct {
|
|
||||||
Root string `json:"root"`
|
|
||||||
Pattern []string `json:"pattern"`
|
|
||||||
TagIDs []int64 `json:"tag_ids"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) HandleReset(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
log.Println("[api] Reset database")
|
|
||||||
|
|
||||||
// reset
|
|
||||||
err = api.Db.ResetFiles()
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = api.Db.ResetFolder()
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.HandleStatus(w, r, "Database reseted")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) HandleWalk(w http.ResponseWriter, r *http.Request) {
|
|
||||||
walkRequest := &WalkRequest{}
|
|
||||||
err := json.NewDecoder(r.Body).Decode(walkRequest)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// check root empty
|
|
||||||
if walkRequest.Root == "" {
|
|
||||||
api.HandleErrorString(w, r, `key "root" can't be empty`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// check pattern empty
|
|
||||||
if len(walkRequest.Pattern) == 0 {
|
|
||||||
api.HandleErrorString(w, r, `"[]pattern" can't be empty`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// get userID
|
|
||||||
userID, err := api.GetUserID(w, r)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("[api] Walk", walkRequest.Root, walkRequest.Pattern, walkRequest.TagIDs)
|
|
||||||
|
|
||||||
// walk
|
|
||||||
err = api.Db.Walk(walkRequest.Root, walkRequest.Pattern, walkRequest.TagIDs, userID)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.HandleStatus(w, r, "Database udpated")
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrNotLoggedIn = errors.New("not logged in")
|
|
||||||
ErrNotAdmin = errors.New("not admin")
|
|
||||||
ErrEmpty = errors.New("Empty field detected, please fill in all fields")
|
|
||||||
ErrAnonymous = errors.New("Anonymous user detected, please login")
|
|
||||||
ErrNotActive = errors.New("User is not active")
|
|
||||||
ErrWrongPassword = errors.New("Wrong password")
|
|
||||||
)
|
|
||||||
|
|
||||||
type Error struct {
|
|
||||||
Error string `json:"error,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) HandleError(w http.ResponseWriter, r *http.Request, err error) {
|
|
||||||
api.HandleErrorString(w, r, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) HandleErrorCode(w http.ResponseWriter, r *http.Request, err error, code int) {
|
|
||||||
api.HandleErrorStringCode(w, r, err.Error(), code)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) HandleErrorString(w http.ResponseWriter, r *http.Request, errorString string) {
|
|
||||||
api.HandleErrorStringCode(w, r, errorString, 500)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) HandleErrorStringCode(w http.ResponseWriter, r *http.Request, errorString string, code int) {
|
|
||||||
log.Println("[api] [Error]", code, errorString)
|
|
||||||
errStatus := &Error{
|
|
||||||
Error: errorString,
|
|
||||||
}
|
|
||||||
w.WriteHeader(code)
|
|
||||||
json.NewEncoder(w).Encode(errStatus)
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"log"
|
|
||||||
"msw-open-music/pkg/database"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type FeedbackRequest struct {
|
|
||||||
Content string `json:"content"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) HandleFeedback(w http.ResponseWriter, r *http.Request) {
|
|
||||||
feedbackRequest := &FeedbackRequest{}
|
|
||||||
err := json.NewDecoder(r.Body).Decode(feedbackRequest)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// check empty feedback
|
|
||||||
if feedbackRequest.Content == "" {
|
|
||||||
api.HandleErrorString(w, r, `"feedback" can't be empty`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("[api] Feedback", feedbackRequest.Content)
|
|
||||||
|
|
||||||
headerBuff := &bytes.Buffer{}
|
|
||||||
err = r.Header.Write(headerBuff)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
header := headerBuff.String()
|
|
||||||
|
|
||||||
userID, err := api.GetUserID(w, r)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = api.Db.InsertFeedback(time.Now().Unix(), feedbackRequest.Content, userID, header)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
api.HandleOK(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetFeedbacksResponse struct {
|
|
||||||
Feedbacks []*database.Feedback `json:"feedbacks"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) HandleGetFeedbacks(w http.ResponseWriter, r *http.Request) {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"log"
|
|
||||||
"msw-open-music/pkg/commonconfig"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (api *API) GetFfmpegConfig(configName string) (commonconfig.FfmpegConfig, bool) {
|
|
||||||
ffmpegConfig := commonconfig.FfmpegConfig{}
|
|
||||||
for _, f := range api.APIConfig.FfmpegConfigList {
|
|
||||||
if f.Name == configName {
|
|
||||||
ffmpegConfig = f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ffmpegConfig.Name == "" {
|
|
||||||
return ffmpegConfig, false
|
|
||||||
}
|
|
||||||
return ffmpegConfig, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) HandleGetFfmpegConfigs(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Println("[api] Get ffmpeg config list")
|
|
||||||
ffmpegConfigList := &commonconfig.FfmpegConfigList{
|
|
||||||
FfmpegConfigList: api.APIConfig.FfmpegConfigList,
|
|
||||||
}
|
|
||||||
json.NewEncoder(w).Encode(&ffmpegConfigList)
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
type GetFileRequest struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) HandleGetFileInfo(w http.ResponseWriter, r *http.Request) {
|
|
||||||
getFileRequest := &GetFileRequest{
|
|
||||||
ID: -1,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(getFileRequest)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// check empty
|
|
||||||
if getFileRequest.ID < 0 {
|
|
||||||
api.HandleErrorString(w, r, `"id" can't be none or negative`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("[api] Get file info", getFileRequest.ID)
|
|
||||||
|
|
||||||
file, err := api.Db.GetFile(getFileRequest.ID)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode(file)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// /get_file
|
|
||||||
// get raw file with io.Copy method
|
|
||||||
func (api *API) HandleGetFile(w http.ResponseWriter, r *http.Request) {
|
|
||||||
getFileRequest := &GetFileRequest{
|
|
||||||
ID: -1,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(getFileRequest)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// check empty
|
|
||||||
if getFileRequest.ID < 0 {
|
|
||||||
api.HandleErrorString(w, r, `"id" can't be none or negative`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := api.Db.GetFile(getFileRequest.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 pipe raw file", path)
|
|
||||||
|
|
||||||
src, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer src.Close()
|
|
||||||
io.Copy(w, src)
|
|
||||||
}
|
|
||||||
|
|
||||||
// /get_file_direct?id=1
|
|
||||||
// get raw file with http.ServeFile method
|
|
||||||
func (api *API) HandleGetFileDirect(w http.ResponseWriter, r *http.Request) {
|
|
||||||
q := r.URL.Query()
|
|
||||||
ids := q["id"]
|
|
||||||
if len(ids) == 0 {
|
|
||||||
api.HandleErrorString(w, r, `parameter "id" can't be empty`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
id, err := strconv.Atoi(ids[0])
|
|
||||||
if err != nil {
|
|
||||||
api.HandleErrorString(w, r, `parameter "id" should be an integer`)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// set header for filename
|
|
||||||
filename := file.Filename
|
|
||||||
// encode filename to URL
|
|
||||||
filename = url.PathEscape(filename)
|
|
||||||
w.Header().Set("Content-Disposition", "inline; filename*=UTF-8''"+filename)
|
|
||||||
|
|
||||||
log.Println("[api] Get direct raw file", path)
|
|
||||||
|
|
||||||
http.ServeFile(w, r, path)
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"log"
|
|
||||||
"msw-open-music/pkg/database"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type GetFilesInFolderRequest struct {
|
|
||||||
Folder_id int64 `json:"folder_id"`
|
|
||||||
Limit int64 `json:"limit"`
|
|
||||||
Offset int64 `json:"offset"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetFilesInFolderResponse struct {
|
|
||||||
Files *[]database.File `json:"files"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) HandleGetFilesInFolder(w http.ResponseWriter, r *http.Request) {
|
|
||||||
getFilesInFolderRequest := &GetFilesInFolderRequest{
|
|
||||||
Folder_id: -1,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(getFilesInFolderRequest)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// check empyt
|
|
||||||
if getFilesInFolderRequest.Folder_id < 0 {
|
|
||||||
api.HandleErrorString(w, r, `"folder_id" can't be none or negative`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
files, err := api.Db.GetFilesInFolder(getFilesInFolderRequest.Folder_id, getFilesInFolderRequest.Limit, getFilesInFolderRequest.Offset)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
getFilesInFolderResponse := &GetFilesInFolderResponse{
|
|
||||||
Files: &files,
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("[api] Get files in folder", getFilesInFolderRequest.Folder_id)
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(getFilesInFolderResponse)
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"log"
|
|
||||||
"msw-open-music/pkg/database"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type GetRandomFilesResponse struct {
|
|
||||||
Files *[]database.File `json:"files"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) HandleGetRandomFiles(w http.ResponseWriter, r *http.Request) {
|
|
||||||
files, err := api.Db.GetRandomFiles(10)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
getRandomFilesResponse := &GetRandomFilesResponse{
|
|
||||||
Files: &files,
|
|
||||||
}
|
|
||||||
log.Println("[api] Get random files")
|
|
||||||
json.NewEncoder(w).Encode(getRandomFilesResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetRandomFilesWithTagRequest struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) HandleGetRandomFilesWithTag(w http.ResponseWriter, r *http.Request) {
|
|
||||||
req := &GetRandomFilesWithTagRequest{}
|
|
||||||
err := json.NewDecoder(r.Body).Decode(req)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
files, err := api.Db.GetRandomFilesWithTag(req.ID, 10)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
getRandomFilesResponse := &GetRandomFilesResponse{
|
|
||||||
Files: &files,
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("[api] Get random files with tag", req.ID)
|
|
||||||
json.NewEncoder(w).Encode(getRandomFilesResponse)
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"log"
|
|
||||||
"msw-open-music/pkg/database"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SearchFilesRequest struct {
|
|
||||||
Filename string `json:"filename"`
|
|
||||||
Limit int64 `json:"limit"`
|
|
||||||
Offset int64 `json:"offset"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SearchFilesResponse struct {
|
|
||||||
Files []database.File `json:"files"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) HandleSearchFiles(w http.ResponseWriter, r *http.Request) {
|
|
||||||
searchFilesRequest := &SearchFilesRequest{}
|
|
||||||
err := json.NewDecoder(r.Body).Decode(searchFilesRequest)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// check empty
|
|
||||||
if searchFilesRequest.Filename == "" {
|
|
||||||
api.HandleErrorString(w, r, `"filename" can't be empty`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if api.CheckLimit(w, r, searchFilesRequest.Limit) != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
searchFilesResponse := &SearchFilesResponse{}
|
|
||||||
|
|
||||||
searchFilesResponse.Files, err = api.Db.SearchFiles(searchFilesRequest.Filename, searchFilesRequest.Limit, searchFilesRequest.Offset)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("[api] Search files", searchFilesRequest.Filename, searchFilesRequest.Limit, searchFilesRequest.Offset)
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(searchFilesResponse)
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"log"
|
|
||||||
"msw-open-music/pkg/database"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SearchFoldersRequest struct {
|
|
||||||
Foldername string `json:"foldername"`
|
|
||||||
Limit int64 `json:"limit"`
|
|
||||||
Offset int64 `json:"offset"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SearchFoldersResponse struct {
|
|
||||||
Folders []database.Folder `json:"folders"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) HandleSearchFolders(w http.ResponseWriter, r *http.Request) {
|
|
||||||
searchFoldersRequest := &SearchFoldersRequest{}
|
|
||||||
err := json.NewDecoder(r.Body).Decode(searchFoldersRequest)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// check empty
|
|
||||||
if searchFoldersRequest.Foldername == "" {
|
|
||||||
api.HandleErrorString(w, r, `"foldername" can't be empty`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if api.CheckLimit(w, r, searchFoldersRequest.Limit) != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
searchFoldersResponse := &SearchFoldersResponse{}
|
|
||||||
|
|
||||||
searchFoldersResponse.Folders, err = api.Db.SearchFolders(searchFoldersRequest.Foldername, searchFoldersRequest.Limit, searchFoldersRequest.Offset)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("[api] Search folders", searchFoldersRequest.Foldername, searchFoldersRequest.Limit, searchFoldersRequest.Offset)
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(searchFoldersResponse)
|
|
||||||
}
|
|
||||||
@@ -1,211 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"log"
|
|
||||||
"msw-open-music/pkg/database"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (api *API) CheckGetFileStream(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
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 err
|
|
||||||
}
|
|
||||||
_, err = strconv.Atoi(ids[0])
|
|
||||||
if err != nil {
|
|
||||||
err = errors.New(`parameter "id" should be an integer`)
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
configs := q["config"]
|
|
||||||
if len(configs) == 0 {
|
|
||||||
err = errors.New(`parameter "config" can't be empty`)
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// /get_file_stream?id=1&config=ffmpeg_config_name
|
|
||||||
func (api *API) HandleGetFileStream(w http.ResponseWriter, r *http.Request) {
|
|
||||||
err := api.CheckGetFileStream(w, r)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
q := r.URL.Query()
|
|
||||||
ids := q["id"]
|
|
||||||
id, err := strconv.Atoi(ids[0])
|
|
||||||
configs := q["config"]
|
|
||||||
configName := configs[0]
|
|
||||||
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] Stream file", path, configName)
|
|
||||||
|
|
||||||
ffmpegConfig, ok := api.GetFfmpegConfig(configName)
|
|
||||||
if !ok {
|
|
||||||
api.HandleErrorStringCode(w, r, `ffmpeg config not found`, 404)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// set headers for filename
|
|
||||||
filename := file.Filename + "." + ffmpegConfig.Name + "." + ffmpegConfig.Format
|
|
||||||
filename = url.PathEscape(filename)
|
|
||||||
// replace invalid characters
|
|
||||||
w.Header().Set("Content-Disposition", "inline; filename*=UTF-8''"+filename)
|
|
||||||
|
|
||||||
args := strings.Split(ffmpegConfig.Args, " ")
|
|
||||||
startArgs := []string{"-threads", strconv.FormatInt(api.APIConfig.FfmpegThreads, 10), "-i", path}
|
|
||||||
endArgs := []string{"-f", ffmpegConfig.Format, "-"}
|
|
||||||
ffmpegArgs := append(startArgs, args...)
|
|
||||||
ffmpegArgs = append(ffmpegArgs, endArgs...)
|
|
||||||
cmd := exec.Command("ffmpeg", ffmpegArgs...)
|
|
||||||
cmd.Stdout = w
|
|
||||||
// cmd.Stderr = os.Stderr
|
|
||||||
err = cmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type PrepareFileStreamDirectRequest struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
ConfigName string `json:"config_name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PrepareFileStreamDirectResponse struct {
|
|
||||||
File *database.File `json:"file"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// /prepare_file_stream_direct?id=1&config=ffmpeg_config_name
|
|
||||||
func (api *API) HandlePrepareFileStreamDirect(w http.ResponseWriter, r *http.Request) {
|
|
||||||
prepareFileStreamDirectRequst := &PrepareFileStreamDirectRequest{
|
|
||||||
ID: -1,
|
|
||||||
}
|
|
||||||
err := json.NewDecoder(r.Body).Decode(prepareFileStreamDirectRequst)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// check empty
|
|
||||||
if prepareFileStreamDirectRequst.ID < 0 {
|
|
||||||
api.HandleErrorString(w, r, `"id" can't be none or negative`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if prepareFileStreamDirectRequst.ConfigName == "" {
|
|
||||||
api.HandleErrorString(w, r, `"config_name" can't be empty`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
file, err := api.Db.GetFile(prepareFileStreamDirectRequst.ID)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
srcPath, err := file.Path()
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("[api] Prepare stream direct file", srcPath, prepareFileStreamDirectRequst.ConfigName)
|
|
||||||
ffmpegConfig, ok := api.GetFfmpegConfig(prepareFileStreamDirectRequst.ConfigName)
|
|
||||||
if !ok {
|
|
||||||
api.HandleErrorStringCode(w, r, `ffmpeg config not found`, 404)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
objPath := api.Tmpfs.GetObjFilePath(prepareFileStreamDirectRequst.ID, ffmpegConfig)
|
|
||||||
|
|
||||||
// check obj file exists
|
|
||||||
exists := api.Tmpfs.Exits(objPath)
|
|
||||||
if !exists {
|
|
||||||
// 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 {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.Tmpfs.Record(objPath)
|
|
||||||
api.Tmpfs.Unlock(objPath)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fileInfo, err := os.Stat(objPath)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
file.Filesize = fileInfo.Size()
|
|
||||||
|
|
||||||
prepareFileStreamDirectResponse := &PrepareFileStreamDirectResponse{
|
|
||||||
File: file,
|
|
||||||
}
|
|
||||||
json.NewEncoder(w).Encode(prepareFileStreamDirectResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
// /get_file_stream_direct?id=1&config=ffmpeg_config_name
|
|
||||||
// return converted file with http.ServeFile method
|
|
||||||
func (api *API) HandleGetFileStreamDirect(w http.ResponseWriter, r *http.Request) {
|
|
||||||
err := api.CheckGetFileStream(w, r)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
q := r.URL.Query()
|
|
||||||
ids := q["id"]
|
|
||||||
id, err := strconv.Atoi(ids[0])
|
|
||||||
configs := q["config"]
|
|
||||||
configName := configs[0]
|
|
||||||
|
|
||||||
ffmpegConfig, ok := api.GetFfmpegConfig(configName)
|
|
||||||
if !ok {
|
|
||||||
api.HandleErrorStringCode(w, r, `ffmpeg config not found`, 404)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
path := api.Tmpfs.GetObjFilePath(int64(id), ffmpegConfig)
|
|
||||||
if api.Tmpfs.Exits(path) {
|
|
||||||
api.Tmpfs.Record(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// set headers for filename
|
|
||||||
filename := ids[0] + "." + ffmpegConfig.Name + "." + ffmpegConfig.Format
|
|
||||||
filename = url.PathEscape(filename)
|
|
||||||
// replace invalid characters
|
|
||||||
w.Header().Set("Content-Disposition", "inline; filename*=UTF-8''"+filename)
|
|
||||||
|
|
||||||
log.Println("[api] Get direct cached file", path)
|
|
||||||
|
|
||||||
http.ServeFile(w, r, path)
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
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,322 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
|
||||||
"log"
|
|
||||||
"msw-open-music/pkg/database"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LoginRequest struct {
|
|
||||||
Username string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LoginResponse struct {
|
|
||||||
User *database.User `json:"user"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) LoginAsAnonymous(w http.ResponseWriter, r *http.Request) {
|
|
||||||
user, err := api.Db.LoginAsAnonymous()
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
session, _ := api.store.Get(r, api.defaultSessionName)
|
|
||||||
|
|
||||||
// save session
|
|
||||||
session.Values["userId"] = user.ID
|
|
||||||
err = session.Save(r, w)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := &LoginResponse{
|
|
||||||
User: user,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode(resp)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) HandleLogin(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var user *database.User
|
|
||||||
var err error
|
|
||||||
session, _ := api.store.Get(r, api.defaultSessionName)
|
|
||||||
|
|
||||||
// Get method will login current or anonymous user
|
|
||||||
if r.Method == "GET" {
|
|
||||||
|
|
||||||
// if user already logged in
|
|
||||||
if userId, ok := session.Values["userId"]; ok {
|
|
||||||
user, err = api.Db.GetUserById(userId.(int64))
|
|
||||||
if err != nil {
|
|
||||||
if err != sql.ErrNoRows {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Println("[api] Warning: User not found")
|
|
||||||
// login as anonymous user
|
|
||||||
api.LoginAsAnonymous(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Println("[api] User already logged in:", user)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// login as anonymous user
|
|
||||||
log.Println("[api] Login as anonymous user")
|
|
||||||
api.LoginAsAnonymous(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
var request LoginRequest
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&request)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("Login as user", request.Username)
|
|
||||||
|
|
||||||
user, err = api.Db.Login(request.Username, request.Password)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if user is not active
|
|
||||||
if !user.Active {
|
|
||||||
api.HandleError(w, r, ErrNotActive)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// save session
|
|
||||||
session.Values["userId"] = user.ID
|
|
||||||
err = session.Save(r, w)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := &LoginResponse{
|
|
||||||
User: user,
|
|
||||||
}
|
|
||||||
err = json.NewEncoder(w).Encode(resp)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type RegisterRequest struct {
|
|
||||||
Username string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
Role int64 `json:"role"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) HandleRegister(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var request RegisterRequest
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&request)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("[api] Register user", request.Username)
|
|
||||||
|
|
||||||
err = api.Db.Register(request.Username, request.Password, request.Role)
|
|
||||||
if err != nil {
|
|
||||||
api.HandleError(w, r, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.HandleOK(w, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) 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)
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Database struct {
|
|
||||||
sqlConn *sql.DB
|
|
||||||
stmt *Stmt
|
|
||||||
singleThreadLock SingleThreadLock
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSingleThreadLock(enabled bool) SingleThreadLock {
|
|
||||||
return SingleThreadLock{
|
|
||||||
lock: sync.Mutex{},
|
|
||||||
enabled: enabled,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type SingleThreadLock struct {
|
|
||||||
lock sync.Mutex
|
|
||||||
enabled bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (stl *SingleThreadLock) Lock() {
|
|
||||||
if stl.enabled {
|
|
||||||
stl.lock.Lock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (stl *SingleThreadLock) Unlock() {
|
|
||||||
if stl.enabled {
|
|
||||||
stl.lock.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDatabase(dbName string, singleThread bool) (*Database, error) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// open database
|
|
||||||
sqlConn, err := sql.Open("sqlite3", dbName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// prepare statement
|
|
||||||
stmt, err := NewPreparedStatement(sqlConn)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// new database
|
|
||||||
database := &Database{
|
|
||||||
sqlConn: sqlConn,
|
|
||||||
stmt: stmt,
|
|
||||||
singleThreadLock: NewSingleThreadLock(singleThread),
|
|
||||||
}
|
|
||||||
|
|
||||||
return database, nil
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrNotFound = errors.New("object not found")
|
|
||||||
ErrTagNotFound = errors.New("tag not found")
|
|
||||||
)
|
|
||||||
@@ -1,399 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (database *Database) GetRandomFiles(limit int64) ([]File, error) {
|
|
||||||
database.singleThreadLock.Lock()
|
|
||||||
defer database.singleThreadLock.Unlock()
|
|
||||||
|
|
||||||
rows, err := database.stmt.getRandomFiles.Query(limit)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
files := make([]File, 0)
|
|
||||||
for rows.Next() {
|
|
||||||
file := File{
|
|
||||||
Db: database,
|
|
||||||
}
|
|
||||||
err = rows.Scan(&file.ID, &file.Folder_id, &file.Filename, &file.Foldername, &file.Filesize)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
files = append(files, file)
|
|
||||||
}
|
|
||||||
return files, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (database *Database) GetRandomFilesWithTag(tagID, limit int64) ([]File, error) {
|
|
||||||
database.singleThreadLock.Lock()
|
|
||||||
defer database.singleThreadLock.Unlock()
|
|
||||||
|
|
||||||
rows, err := database.stmt.getRandomFilesWithTag.Query(tagID, limit)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
files := make([]File, 0)
|
|
||||||
for rows.Next() {
|
|
||||||
file := File{
|
|
||||||
Db: database,
|
|
||||||
}
|
|
||||||
err = rows.Scan(&file.ID, &file.Folder_id, &file.Filename, &file.Foldername, &file.Filesize)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
files = append(files, file)
|
|
||||||
}
|
|
||||||
return files, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (database *Database) GetFilesInFolder(folder_id int64, limit int64, offset int64) ([]File, error) {
|
|
||||||
database.singleThreadLock.Lock()
|
|
||||||
defer database.singleThreadLock.Unlock()
|
|
||||||
|
|
||||||
rows, err := database.stmt.getFilesInFolder.Query(folder_id, limit, offset)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
files := make([]File, 0)
|
|
||||||
for rows.Next() {
|
|
||||||
file := File{
|
|
||||||
Db: database,
|
|
||||||
Folder_id: folder_id,
|
|
||||||
}
|
|
||||||
err = rows.Scan(&file.ID, &file.Filename, &file.Filesize, &file.Foldername)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
files = append(files, file)
|
|
||||||
}
|
|
||||||
return files, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (database *Database) SearchFolders(foldername string, limit int64, offset int64) ([]Folder, error) {
|
|
||||||
database.singleThreadLock.Lock()
|
|
||||||
defer database.singleThreadLock.Unlock()
|
|
||||||
|
|
||||||
rows, err := database.stmt.searchFolders.Query("%"+foldername+"%", limit, offset)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("Error searching folders at query " + err.Error())
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
folders := make([]Folder, 0)
|
|
||||||
for rows.Next() {
|
|
||||||
folder := Folder{
|
|
||||||
Db: database,
|
|
||||||
}
|
|
||||||
err = rows.Scan(&folder.ID, &folder.Folder, &folder.Foldername)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("Error scanning SearchFolders" + err.Error())
|
|
||||||
}
|
|
||||||
folders = append(folders, folder)
|
|
||||||
}
|
|
||||||
return folders, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (database *Database) GetFile(id int64) (*File, error) {
|
|
||||||
file := &File{
|
|
||||||
Db: database,
|
|
||||||
}
|
|
||||||
|
|
||||||
database.singleThreadLock.Lock()
|
|
||||||
defer database.singleThreadLock.Unlock()
|
|
||||||
|
|
||||||
err := database.stmt.getFile.QueryRow(id).Scan(&file.ID, &file.Folder_id, &file.Realname, &file.Filename, &file.Foldername, &file.Filesize)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return file, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (database *Database) ResetFiles() error {
|
|
||||||
log.Println("[db] Reset files")
|
|
||||||
var err error
|
|
||||||
_, err = database.stmt.dropFiles.Exec()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = database.stmt.initFilesTable.Exec()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (database *Database) ResetFolder() error {
|
|
||||||
log.Println("[db] Reset folders")
|
|
||||||
var err error
|
|
||||||
_, err = database.stmt.dropFolder.Exec()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = database.stmt.initFoldersTable.Exec()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (database *Database) Walk(root string, pattern []string, tagIDs []int64, userID int64) error {
|
|
||||||
patternDict := make(map[string]bool)
|
|
||||||
for _, v := range pattern {
|
|
||||||
patternDict[v] = true
|
|
||||||
}
|
|
||||||
log.Println("[db] Walk", root, patternDict)
|
|
||||||
|
|
||||||
tags := make([]*Tag, 0)
|
|
||||||
for _, tagID := range tagIDs {
|
|
||||||
tag, err := database.GetTag(tagID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
tags = append(tags, tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.IsDir() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// check pattern
|
|
||||||
ext := filepath.Ext(info.Name())
|
|
||||||
if _, ok := patternDict[ext]; !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// insert file, folder will aut created
|
|
||||||
fileID, err := database.Insert(path, info.Size())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tag := range tags {
|
|
||||||
err = database.PutTagOnFile(tag.ID, fileID, userID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (database *Database) GetFolder(folderId int64) (*Folder, error) {
|
|
||||||
folder := &Folder{
|
|
||||||
Db: database,
|
|
||||||
}
|
|
||||||
|
|
||||||
database.singleThreadLock.Lock()
|
|
||||||
defer database.singleThreadLock.Unlock()
|
|
||||||
|
|
||||||
err := database.stmt.getFolder.QueryRow(folderId).Scan(&folder.Folder)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return folder, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (database *Database) SearchFiles(filename string, limit int64, offset int64) ([]File, error) {
|
|
||||||
database.singleThreadLock.Lock()
|
|
||||||
defer database.singleThreadLock.Unlock()
|
|
||||||
|
|
||||||
rows, err := database.stmt.searchFiles.Query("%"+filename+"%", limit, offset)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("Error searching files at query " + err.Error())
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
files := make([]File, 0)
|
|
||||||
for rows.Next() {
|
|
||||||
var file File = File{
|
|
||||||
Db: database,
|
|
||||||
}
|
|
||||||
err = rows.Scan(&file.ID, &file.Folder_id, &file.Filename, &file.Foldername, &file.Filesize)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("Error scanning SearchFiles " + err.Error())
|
|
||||||
}
|
|
||||||
files = append(files, file)
|
|
||||||
}
|
|
||||||
if err = rows.Err(); err != nil {
|
|
||||||
return nil, errors.New("Error scanning SearchFiles exit without full result" + err.Error())
|
|
||||||
}
|
|
||||||
return files, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (database *Database) FindFolder(folder string) (int64, error) {
|
|
||||||
var id int64
|
|
||||||
|
|
||||||
database.singleThreadLock.Lock()
|
|
||||||
defer database.singleThreadLock.Unlock()
|
|
||||||
|
|
||||||
err := database.stmt.findFolder.QueryRow(folder).Scan(&id)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (database *Database) FindFile(folderId int64, filename string) (int64, error) {
|
|
||||||
var id int64
|
|
||||||
|
|
||||||
database.singleThreadLock.Lock()
|
|
||||||
defer database.singleThreadLock.Unlock()
|
|
||||||
|
|
||||||
err := database.stmt.findFile.QueryRow(folderId, filename).Scan(&id)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (database *Database) InsertFolder(folder string) (int64, error) {
|
|
||||||
|
|
||||||
database.singleThreadLock.Lock()
|
|
||||||
defer database.singleThreadLock.Unlock()
|
|
||||||
|
|
||||||
result, err := database.stmt.insertFolder.Exec(folder, filepath.Base(folder))
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
lastInsertId, err := result.LastInsertId()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return lastInsertId, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (database *Database) InsertFile(folderId int64, filename string, filesize int64) (int64, error) {
|
|
||||||
database.singleThreadLock.Lock()
|
|
||||||
defer database.singleThreadLock.Unlock()
|
|
||||||
|
|
||||||
result, err := database.stmt.insertFile.Exec(folderId, filename, filename, filesize)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
lastInsertId, err := result.LastInsertId()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return lastInsertId, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (database *Database) Insert(path string, filesize int64) (int64, error) {
|
|
||||||
folder, filename := filepath.Split(path)
|
|
||||||
folderId, err := database.FindFolder(folder)
|
|
||||||
if err != nil {
|
|
||||||
folderId, err = database.InsertFolder(folder)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if file exists, skip it
|
|
||||||
lastInsertId, err := database.FindFile(folderId, filename)
|
|
||||||
if err == nil {
|
|
||||||
return lastInsertId, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
lastInsertId, err = database.InsertFile(folderId, filename, filesize)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return lastInsertId, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (database *Database) UpdateFoldername(folderId int64, foldername string) error {
|
|
||||||
database.singleThreadLock.Lock()
|
|
||||||
defer database.singleThreadLock.Unlock()
|
|
||||||
|
|
||||||
_, err := database.stmt.updateFoldername.Exec(foldername, folderId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (database *Database) DeleteFile(fileId int64) error {
|
|
||||||
database.singleThreadLock.Lock()
|
|
||||||
defer database.singleThreadLock.Unlock()
|
|
||||||
|
|
||||||
// begin transaction
|
|
||||||
tx, err := database.sqlConn.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete file
|
|
||||||
_, err = tx.Stmt(database.stmt.deleteFile).Exec(fileId)
|
|
||||||
if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete tag on file
|
|
||||||
_, err = tx.Stmt(database.stmt.deleteFileReferenceInFileHasTag).Exec(fileId)
|
|
||||||
if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete reviews on file
|
|
||||||
_, err = tx.Stmt(database.stmt.deleteFileReferenceInReviews).Exec(fileId)
|
|
||||||
if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// commit transaction
|
|
||||||
err = tx.Commit()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (database *Database) UpdateFilename(fileId int64, filename string) error {
|
|
||||||
database.singleThreadLock.Lock()
|
|
||||||
defer database.singleThreadLock.Unlock()
|
|
||||||
|
|
||||||
_, err := database.stmt.updateFilename.Exec(filename, fileId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (database *Database) ResetFilename(fileId int64) error {
|
|
||||||
database.singleThreadLock.Lock()
|
|
||||||
defer database.singleThreadLock.Unlock()
|
|
||||||
|
|
||||||
_, err := database.stmt.resetFilename.Exec(fileId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (database *Database) ResetFoldername(folderId int64) error {
|
|
||||||
folder, err := database.GetFolder(folderId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
foldername := filepath.Base(folder.Folder)
|
|
||||||
err = database.UpdateFoldername(folderId, foldername)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
func (database *Database) InsertTag(tag *Tag) (int64, error) {
|
|
||||||
database.singleThreadLock.Lock()
|
|
||||||
defer database.singleThreadLock.Unlock()
|
|
||||||
|
|
||||||
result, err := database.stmt.insertTag.Exec(tag.Name, tag.Description, tag.CreatedByUserId)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
id, err := result.LastInsertId()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (database *Database) GetTag(id int64) (*Tag, error) {
|
|
||||||
database.singleThreadLock.Lock()
|
|
||||||
defer database.singleThreadLock.Unlock()
|
|
||||||
|
|
||||||
tag := &Tag{CreatedByUser: &User{}}
|
|
||||||
err := database.stmt.getTag.QueryRow(id).Scan(
|
|
||||||
&tag.ID, &tag.Name, &tag.Description,
|
|
||||||
&tag.CreatedByUser.ID, &tag.CreatedByUser.Username, &tag.CreatedByUser.Role, &tag.CreatedByUser.AvatarId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return tag, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (database *Database) GetTags() ([]*Tag, error) {
|
|
||||||
tags := []*Tag{}
|
|
||||||
|
|
||||||
database.singleThreadLock.Lock()
|
|
||||||
defer database.singleThreadLock.Unlock()
|
|
||||||
|
|
||||||
rows, err := database.stmt.getTags.Query()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
for rows.Next() {
|
|
||||||
tag := &Tag{CreatedByUser: &User{}}
|
|
||||||
err := rows.Scan(
|
|
||||||
&tag.ID, &tag.Name, &tag.Description,
|
|
||||||
&tag.CreatedByUser.ID, &tag.CreatedByUser.Username, &tag.CreatedByUser.Role, &tag.CreatedByUser.AvatarId)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tags = append(tags, tag)
|
|
||||||
}
|
|
||||||
return tags, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (database *Database) UpdateTag(tag *Tag) error {
|
|
||||||
database.singleThreadLock.Lock()
|
|
||||||
defer database.singleThreadLock.Unlock()
|
|
||||||
|
|
||||||
result, err := database.stmt.updateTag.Exec(tag.Name, tag.Description, tag.ID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
rowsAffected, err := result.RowsAffected()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if rowsAffected == 0 {
|
|
||||||
return errors.New("No rows affected")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete tag and all its references in file_has_tag
|
|
||||||
func (database *Database) DeleteTag(id int64) error {
|
|
||||||
database.singleThreadLock.Lock()
|
|
||||||
defer database.singleThreadLock.Unlock()
|
|
||||||
|
|
||||||
// begin transaction
|
|
||||||
tx, err := database.sqlConn.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete tag
|
|
||||||
_, err = tx.Stmt(database.stmt.deleteTag).Exec(id)
|
|
||||||
if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete file_has_tag
|
|
||||||
_, err = tx.Stmt(database.stmt.deleteTagReferenceInFileHasTag).Exec(id)
|
|
||||||
if err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// commit transaction
|
|
||||||
err = tx.Commit()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,776 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
)
|
|
||||||
|
|
||||||
var initFilesTableQuery = `CREATE TABLE IF NOT EXISTS files (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
folder_id INTEGER NOT NULL,
|
|
||||||
realname TEXT NOT NULL,
|
|
||||||
filename TEXT NOT NULL,
|
|
||||||
filesize INTEGER NOT NULL,
|
|
||||||
FOREIGN KEY(folder_id) REFERENCES folders(id)
|
|
||||||
);`
|
|
||||||
|
|
||||||
var initFoldersTableQuery = `CREATE TABLE IF NOT EXISTS folders (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
folder TEXT NOT NULL,
|
|
||||||
foldername TEXT NOT NULL
|
|
||||||
);`
|
|
||||||
|
|
||||||
var initFeedbacksTableQuery = `CREATE TABLE IF NOT EXISTS feedbacks (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
time INTEGER NOT NULL,
|
|
||||||
content TEXT NOT NULL,
|
|
||||||
user_id INTEGER NOT NULL,
|
|
||||||
header TEXT NOT NULL
|
|
||||||
);`
|
|
||||||
|
|
||||||
// User table schema definition
|
|
||||||
// role: 0 - Anonymous User, 1 - Admin, 2 - User
|
|
||||||
var initUsersTableQuery = `CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
username TEXT NOT NULL UNIQUE,
|
|
||||||
password TEXT NOT NULL,
|
|
||||||
role INTEGER NOT NULL,
|
|
||||||
active BOOLEAN NOT NULL,
|
|
||||||
avatar_id INTEGER NOT NULL,
|
|
||||||
FOREIGN KEY(avatar_id) REFERENCES avatars(id)
|
|
||||||
);`
|
|
||||||
|
|
||||||
var initAvatarsTableQuery = `CREATE TABLE IF NOT EXISTS avatars (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
avatarname TEXT NOT NULL,
|
|
||||||
avatar BLOB NOT NULL
|
|
||||||
);`
|
|
||||||
|
|
||||||
var initTagsTableQuery = `CREATE TABLE IF NOT EXISTS tags (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
name TEXT NOT NULL UNIQUE,
|
|
||||||
description TEXT NOT NULL,
|
|
||||||
created_by_user_id INTEGER NOT NULL,
|
|
||||||
FOREIGN KEY(created_by_user_id) REFERENCES users(id)
|
|
||||||
);`
|
|
||||||
|
|
||||||
var initFileHasTagTableQuery = `CREATE TABLE IF NOT EXISTS file_has_tag (
|
|
||||||
file_id INTEGER NOT NULL,
|
|
||||||
tag_id INTEGER NOT NULL,
|
|
||||||
user_id INTEGER NOT NULL,
|
|
||||||
PRIMARY KEY (file_id, tag_id),
|
|
||||||
FOREIGN KEY(user_id) REFERENCES users(id)
|
|
||||||
FOREIGN KEY (file_id) REFERENCES files(id),
|
|
||||||
FOREIGN KEY (tag_id) REFERENCES tags(id)
|
|
||||||
);`
|
|
||||||
|
|
||||||
var initLikesTableQuery = `CREATE TABLE IF NOT EXISTS likes (
|
|
||||||
user_id INTEGER NOT NULL,
|
|
||||||
file_id INTEGER NOT NULL,
|
|
||||||
PRIMARY KEY (user_id, file_id),
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
|
||||||
FOREIGN KEY (file_id) REFERENCES files(id)
|
|
||||||
);`
|
|
||||||
|
|
||||||
var initReviewsTableQuery = `CREATE TABLE IF NOT EXISTS reviews (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
user_id INTEGER NOT NULL,
|
|
||||||
file_id INTEGER NOT NULL,
|
|
||||||
created_at INTEGER NOT NULL,
|
|
||||||
updated_at INTEGER NOT NULL DEFAULT 0,
|
|
||||||
content TEXT NOT NULL,
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
|
||||||
FOREIGN KEY (file_id) REFERENCES files(id)
|
|
||||||
);`
|
|
||||||
|
|
||||||
var initPlaybacksTableQuery = `CREATE TABLE IF NOT EXISTS playbacks (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
user_id INTEGER NOT NULL,
|
|
||||||
file_id INTEGER NOT NULL,
|
|
||||||
time INTEGER NOT NULL,
|
|
||||||
mothod INTEGER NOT NULL,
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
|
||||||
FOREIGN KEY (file_id) REFERENCES files(id)
|
|
||||||
);`
|
|
||||||
|
|
||||||
var initLogsTableQuery = `CREATE TABLE IF NOT EXISTS logs (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
time INTEGER NOT NULL,
|
|
||||||
message TEXT NOT NULL,
|
|
||||||
user_id INTEGER NOT NULL,
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
||||||
);`
|
|
||||||
|
|
||||||
var initTmpfsTableQuery = `CREATE TABLE IF NOT EXISTS tmpfs (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
path TEXT NOT NULL,
|
|
||||||
size INTEGER NOT NULL,
|
|
||||||
file_id INTEGER NOT NULL,
|
|
||||||
ffmpeg_config TEXT NOT NULL,
|
|
||||||
created_time INTEGER NOT NULL,
|
|
||||||
accessed_time INTEGER NOT NULL,
|
|
||||||
FOREIGN KEY (file_id) REFERENCES files(id)
|
|
||||||
);`
|
|
||||||
|
|
||||||
var insertFolderQuery = `INSERT INTO folders (folder, foldername)
|
|
||||||
VALUES (?, ?);`
|
|
||||||
|
|
||||||
var findFolderQuery = `SELECT id FROM folders WHERE folder = ? LIMIT 1;`
|
|
||||||
|
|
||||||
var findFileQuery = `SELECT id FROM files WHERE folder_id = ? AND realname = ? LIMIT 1;`
|
|
||||||
|
|
||||||
var insertFileQuery = `INSERT INTO files (folder_id, realname, filename, filesize)
|
|
||||||
VALUES (?, ?, ?, ?);`
|
|
||||||
|
|
||||||
var searchFilesQuery = `SELECT
|
|
||||||
files.id, files.folder_id, files.filename, folders.foldername, files.filesize
|
|
||||||
FROM files
|
|
||||||
JOIN folders ON files.folder_id = folders.id
|
|
||||||
WHERE filename LIKE ?
|
|
||||||
ORDER BY folders.foldername, files.filename
|
|
||||||
LIMIT ? OFFSET ?;`
|
|
||||||
|
|
||||||
var getFolderQuery = `SELECT folder FROM folders WHERE id = ? LIMIT 1;`
|
|
||||||
|
|
||||||
var dropFilesQuery = `DROP TABLE files;`
|
|
||||||
|
|
||||||
var dropFolderQuery = `DROP TABLE folders;`
|
|
||||||
|
|
||||||
var getFileQuery = `SELECT
|
|
||||||
files.id, files.folder_id, files.realname, files.filename, folders.foldername, files.filesize
|
|
||||||
FROM files
|
|
||||||
JOIN folders ON files.folder_id = folders.id
|
|
||||||
WHERE files.id = ?
|
|
||||||
LIMIT 1;`
|
|
||||||
|
|
||||||
var searchFoldersQuery = `SELECT
|
|
||||||
id, folder, foldername
|
|
||||||
FROM folders
|
|
||||||
WHERE foldername LIKE ?
|
|
||||||
ORDER BY foldername
|
|
||||||
LIMIT ? OFFSET ?;`
|
|
||||||
|
|
||||||
var getFilesInFolderQuery = `SELECT
|
|
||||||
files.id, files.filename, files.filesize, folders.foldername
|
|
||||||
FROM files
|
|
||||||
JOIN folders ON files.folder_id = folders.id
|
|
||||||
WHERE folder_id = ?
|
|
||||||
ORDER BY files.filename
|
|
||||||
LIMIT ? OFFSET ?;`
|
|
||||||
|
|
||||||
var getRandomFilesQuery = `SELECT
|
|
||||||
files.id, files.folder_id, files.filename, folders.foldername, files.filesize
|
|
||||||
FROM files
|
|
||||||
JOIN folders ON files.folder_id = folders.id
|
|
||||||
ORDER BY RANDOM()
|
|
||||||
LIMIT ?;`
|
|
||||||
|
|
||||||
var getRandomFilesWithTagQuery = `SELECT
|
|
||||||
files.id, files.folder_id, files.filename, folders.foldername, files.filesize
|
|
||||||
FROM file_has_tag
|
|
||||||
JOIN files ON file_has_tag.file_id = files.id
|
|
||||||
JOIN folders ON files.folder_id = folders.id
|
|
||||||
WHERE file_has_tag.tag_id = ?
|
|
||||||
ORDER BY RANDOM()
|
|
||||||
LIMIT ?;`
|
|
||||||
|
|
||||||
var insertFeedbackQuery = `INSERT INTO feedbacks (time, content, user_id, header)
|
|
||||||
VALUES (?, ?, ?, ?);`
|
|
||||||
|
|
||||||
var getFeedbacksQuery = `SELECT
|
|
||||||
feedbacks.id, feedbacks.time, feedbacks.content, feedbacks.header,
|
|
||||||
users.id, users.username, users.role, users.active, users.avatar_id
|
|
||||||
FROM feedbacks
|
|
||||||
JOIN users ON feedbacks.user_id = users.id
|
|
||||||
ORDER BY feedbacks.time
|
|
||||||
;`
|
|
||||||
|
|
||||||
var deleteFeedbackQuery = `DELETE FROM feedbacks WHERE id = ?;`
|
|
||||||
|
|
||||||
var insertUserQuery = `INSERT INTO users (username, password, role, active, avatar_id)
|
|
||||||
VALUES (?, ?, ?, ?, ?);`
|
|
||||||
|
|
||||||
var countUserQuery = `SELECT count(*) FROM users;`
|
|
||||||
|
|
||||||
var countAdminQuery = `SELECT count(*) FROM users WHERE role= 1;`
|
|
||||||
|
|
||||||
var getUserQuery = `SELECT id, username, password, role, active, avatar_id FROM users WHERE username = ? LIMIT 1;`
|
|
||||||
|
|
||||||
var getUsersQuery = `SELECT id, username, role, active, avatar_id FROM users;`
|
|
||||||
|
|
||||||
var getUserByIdQuery = `SELECT id, username, role, active, avatar_id FROM users WHERE id = ? LIMIT 1;`
|
|
||||||
|
|
||||||
var updateUserActiveQuery = `UPDATE users SET active = ? WHERE id = ?;`
|
|
||||||
|
|
||||||
var updateUsernameQuery = `UPDATE users SET username = ? WHERE id = ?;`
|
|
||||||
|
|
||||||
var updateUserPasswordQuery = `UPDATE users SET password = ? WHERE id = ?;`
|
|
||||||
|
|
||||||
var getAnonymousUserQuery = `SELECT id, username, role, avatar_id FROM users WHERE role = 0 LIMIT 1;`
|
|
||||||
|
|
||||||
var insertTagQuery = `INSERT INTO tags (name, description, created_by_user_id) VALUES (?, ?, ?);`
|
|
||||||
|
|
||||||
var deleteTagQuery = `DELETE FROM tags WHERE id = ?;`
|
|
||||||
|
|
||||||
var getTagQuery = `SELECT
|
|
||||||
tags.id, tags.name, tags.description,
|
|
||||||
users.id, users.username, users.role, users.avatar_id
|
|
||||||
FROM tags
|
|
||||||
JOIN users ON tags.created_by_user_id = users.id
|
|
||||||
WHERE tags.id = ? LIMIT 1;`
|
|
||||||
|
|
||||||
var getTagsQuery = `SELECT
|
|
||||||
tags.id, tags.name, tags.description,
|
|
||||||
users.id, users.username, users.role, users.avatar_id
|
|
||||||
FROM tags
|
|
||||||
JOIN users ON tags.created_by_user_id = users.id
|
|
||||||
ORDER BY tags.name
|
|
||||||
;`
|
|
||||||
|
|
||||||
var updateTagQuery = `UPDATE tags SET name = ?, description = ? WHERE id = ?;`
|
|
||||||
|
|
||||||
var putTagOnFileQuery = `INSERT OR IGNORE INTO file_has_tag (tag_id, file_id, user_id) VALUES (?, ?, ?);`
|
|
||||||
|
|
||||||
var getTagsOnFileQuery = `SELECT
|
|
||||||
tags.id, tags.name, tags.description, tags.created_by_user_id
|
|
||||||
FROM file_has_tag
|
|
||||||
JOIN tags ON file_has_tag.tag_id = tags.id
|
|
||||||
WHERE file_has_tag.file_id = ?
|
|
||||||
ORDER BY tags.name
|
|
||||||
;`
|
|
||||||
|
|
||||||
var deleteTagOnFileQuery = `DELETE FROM file_has_tag WHERE tag_id = ? AND file_id = ?;`
|
|
||||||
|
|
||||||
var deleteTagReferenceInFileHasTagQuery = `DELETE FROM file_has_tag WHERE tag_id = ?;`
|
|
||||||
|
|
||||||
var updateFoldernameQuery = `UPDATE folders SET foldername = ? WHERE id = ?;`
|
|
||||||
|
|
||||||
var insertReviewQuery = `INSERT INTO reviews (user_id, file_id, created_at, content)
|
|
||||||
VALUES (?, ?, ?, ?);`
|
|
||||||
|
|
||||||
var getReviewsOnFileQuery = `SELECT
|
|
||||||
reviews.id, reviews.created_at, reviews.updated_at, reviews.content,
|
|
||||||
users.id, users.username, users.role, users.avatar_id,
|
|
||||||
files.id, files.filename
|
|
||||||
FROM reviews
|
|
||||||
JOIN users ON reviews.user_id = users.id
|
|
||||||
JOIN files ON reviews.file_id = files.id
|
|
||||||
WHERE reviews.file_id = ?
|
|
||||||
ORDER BY reviews.created_at
|
|
||||||
;`
|
|
||||||
|
|
||||||
var getReviewQuery = `SELECT id, file_id, user_id, created_at, updated_at, content FROM reviews WHERE id = ? LIMIT 1;`
|
|
||||||
|
|
||||||
var updateReviewQuery = `UPDATE reviews SET content = ?, updated_at = ? WHERE id = ?;`
|
|
||||||
|
|
||||||
var deleteReviewQuery = `DELETE FROM reviews WHERE id = ?;`
|
|
||||||
|
|
||||||
var getReviewsByUserQuery = `SELECT
|
|
||||||
reviews.id, reviews.created_at, reviews.updated_at, reviews.content,
|
|
||||||
users.id, users.username, users.role, users.avatar_id,
|
|
||||||
files.id, files.filename
|
|
||||||
FROM reviews
|
|
||||||
JOIN users ON reviews.user_id = users.id
|
|
||||||
JOIN files ON reviews.file_id = files.id
|
|
||||||
WHERE reviews.user_id = ?
|
|
||||||
ORDER BY reviews.created_at
|
|
||||||
;`
|
|
||||||
|
|
||||||
var deleteFileQuery = `DELETE FROM files WHERE id = ?;`
|
|
||||||
|
|
||||||
var deleteFileReferenceInFileHasTagQuery = `DELETE FROM file_has_tag WHERE file_id = ?;`
|
|
||||||
|
|
||||||
var deleteFileReferenceInReviewsQuery = `DELETE FROM reviews WHERE file_id = ?;`
|
|
||||||
|
|
||||||
var updateFilenameQuery = `UPDATE files SET filename = ? WHERE id = ?;`
|
|
||||||
|
|
||||||
var resetFilenameQuery = `UPDATE files SET filename = realname WHERE id = ?;`
|
|
||||||
|
|
||||||
type Stmt struct {
|
|
||||||
initFilesTable *sql.Stmt
|
|
||||||
initFoldersTable *sql.Stmt
|
|
||||||
initFeedbacksTable *sql.Stmt
|
|
||||||
initUsersTable *sql.Stmt
|
|
||||||
initAvatarsTable *sql.Stmt
|
|
||||||
initTagsTable *sql.Stmt
|
|
||||||
initFileHasTag *sql.Stmt
|
|
||||||
initLikesTable *sql.Stmt
|
|
||||||
initReviewsTable *sql.Stmt
|
|
||||||
initPlaybacksTable *sql.Stmt
|
|
||||||
initLogsTable *sql.Stmt
|
|
||||||
initTmpfsTable *sql.Stmt
|
|
||||||
insertFolder *sql.Stmt
|
|
||||||
insertFile *sql.Stmt
|
|
||||||
findFolder *sql.Stmt
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
stmt := &Stmt{}
|
|
||||||
|
|
||||||
// init files table
|
|
||||||
stmt.initFilesTable, err = sqlConn.Prepare(initFilesTableQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init folders table
|
|
||||||
stmt.initFoldersTable, err = sqlConn.Prepare(initFoldersTableQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init feedbacks tables
|
|
||||||
stmt.initFeedbacksTable, err = sqlConn.Prepare(initFeedbacksTableQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init users table
|
|
||||||
stmt.initUsersTable, err = sqlConn.Prepare(initUsersTableQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init avatars table
|
|
||||||
stmt.initAvatarsTable, err = sqlConn.Prepare(initAvatarsTableQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init tags table
|
|
||||||
stmt.initTagsTable, err = sqlConn.Prepare(initTagsTableQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init file_has_tag table
|
|
||||||
stmt.initFileHasTag, err = sqlConn.Prepare(initFileHasTagTableQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init likes table
|
|
||||||
stmt.initLikesTable, err = sqlConn.Prepare(initLikesTableQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init reviews table
|
|
||||||
stmt.initReviewsTable, err = sqlConn.Prepare(initReviewsTableQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init playbacks table
|
|
||||||
stmt.initPlaybacksTable, err = sqlConn.Prepare(initPlaybacksTableQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init logs table
|
|
||||||
stmt.initLogsTable, err = sqlConn.Prepare(initLogsTableQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init tmpfs table
|
|
||||||
stmt.initTmpfsTable, err = sqlConn.Prepare(initTmpfsTableQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// run init statement
|
|
||||||
_, err = stmt.initFilesTable.Exec()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = stmt.initFoldersTable.Exec()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = stmt.initFeedbacksTable.Exec()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = stmt.initUsersTable.Exec()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = stmt.initAvatarsTable.Exec()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = stmt.initTagsTable.Exec()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = stmt.initFileHasTag.Exec()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = stmt.initLikesTable.Exec()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = stmt.initReviewsTable.Exec()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = stmt.initPlaybacksTable.Exec()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = stmt.initLogsTable.Exec()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = stmt.initTmpfsTable.Exec()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init insert folder statement
|
|
||||||
stmt.insertFolder, err = sqlConn.Prepare(insertFolderQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init findFolder statement
|
|
||||||
stmt.findFolder, err = sqlConn.Prepare(findFolderQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init findFile statement
|
|
||||||
stmt.findFile, err = sqlConn.Prepare(findFileQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init insertFile stmt
|
|
||||||
stmt.insertFile, err = sqlConn.Prepare(insertFileQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init searchFile stmt
|
|
||||||
stmt.searchFiles, err = sqlConn.Prepare(searchFilesQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init getFolder stmt
|
|
||||||
stmt.getFolder, err = sqlConn.Prepare(getFolderQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init dropFolder stmt
|
|
||||||
stmt.dropFolder, err = sqlConn.Prepare(dropFolderQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init dropFiles stmt
|
|
||||||
stmt.dropFiles, err = sqlConn.Prepare(dropFilesQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init getFile stmt
|
|
||||||
stmt.getFile, err = sqlConn.Prepare(getFileQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init searchFolder stmt
|
|
||||||
stmt.searchFolders, err = sqlConn.Prepare(searchFoldersQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init getFilesInFolder stmt
|
|
||||||
stmt.getFilesInFolder, err = sqlConn.Prepare(getFilesInFolderQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init getRandomFiles
|
|
||||||
stmt.getRandomFiles, err = sqlConn.Prepare(getRandomFilesQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init getRandomFilesWithTag
|
|
||||||
stmt.getRandomFilesWithTag, err = sqlConn.Prepare(getRandomFilesWithTagQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init insertFeedback
|
|
||||||
stmt.insertFeedback, err = sqlConn.Prepare(insertFeedbackQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init getFeedbacks
|
|
||||||
stmt.getFeedbacks, err = sqlConn.Prepare(getFeedbacksQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init deleteFeedback
|
|
||||||
stmt.deleteFeedback, err = sqlConn.Prepare(deleteFeedbackQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init insertUser
|
|
||||||
stmt.insertUser, err = sqlConn.Prepare(insertUserQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init countUser
|
|
||||||
stmt.countUser, err = sqlConn.Prepare(countUserQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init countAdmin
|
|
||||||
stmt.countAdmin, err = sqlConn.Prepare(countAdminQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init getUser
|
|
||||||
stmt.getUser, err = sqlConn.Prepare(getUserQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init getUsers
|
|
||||||
stmt.getUsers, err = sqlConn.Prepare(getUsersQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init getUserById
|
|
||||||
stmt.getUserById, err = sqlConn.Prepare(getUserByIdQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init updateUserActive
|
|
||||||
stmt.updateUserActive, err = sqlConn.Prepare(updateUserActiveQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init updateUsername
|
|
||||||
stmt.updateUsername, err = sqlConn.Prepare(updateUsernameQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init updateUserPassword
|
|
||||||
stmt.updateUserPassword, err = sqlConn.Prepare(updateUserPasswordQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init getAnonymousUser
|
|
||||||
stmt.getAnonymousUser, err = sqlConn.Prepare(getAnonymousUserQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// insert Anonymous user if users is empty
|
|
||||||
userCount := 0
|
|
||||||
err = stmt.countUser.QueryRow().Scan(&userCount)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if userCount == 0 {
|
|
||||||
_, err = stmt.insertUser.Exec("Anonymous user", "", 0, 1, 0)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// init insertTag
|
|
||||||
stmt.insertTag, err = sqlConn.Prepare(insertTagQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init deleteTag
|
|
||||||
stmt.deleteTag, err = sqlConn.Prepare(deleteTagQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init getTag
|
|
||||||
stmt.getTag, err = sqlConn.Prepare(getTagQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init getTags
|
|
||||||
stmt.getTags, err = sqlConn.Prepare(getTagsQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init updateTag
|
|
||||||
stmt.updateTag, err = sqlConn.Prepare(updateTagQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init putTagOnFile
|
|
||||||
stmt.putTagOnFile, err = sqlConn.Prepare(putTagOnFileQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init getTagsOnFile
|
|
||||||
stmt.getTagsOnFile, err = sqlConn.Prepare(getTagsOnFileQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init deleteTagOnFile
|
|
||||||
stmt.deleteTagOnFile, err = sqlConn.Prepare(deleteTagOnFileQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init deleteTagReferenceInFileHasTag
|
|
||||||
stmt.deleteTagReferenceInFileHasTag, err = sqlConn.Prepare(
|
|
||||||
deleteTagReferenceInFileHasTagQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init updateFoldername
|
|
||||||
stmt.updateFoldername, err = sqlConn.Prepare(updateFoldernameQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init insertReview
|
|
||||||
stmt.insertReview, err = sqlConn.Prepare(insertReviewQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init getReviewsOnFile
|
|
||||||
stmt.getReviewsOnFile, err = sqlConn.Prepare(getReviewsOnFileQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init getReview
|
|
||||||
stmt.getReview, err = sqlConn.Prepare(getReviewQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init updateReview
|
|
||||||
stmt.updateReview, err = sqlConn.Prepare(updateReviewQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init deleteReview
|
|
||||||
stmt.deleteReview, err = sqlConn.Prepare(deleteReviewQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init getReviewsByUser
|
|
||||||
stmt.getReviewsByUser, err = sqlConn.Prepare(getReviewsByUserQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init deleteFile
|
|
||||||
stmt.deleteFile, err = sqlConn.Prepare(deleteFileQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init deleteFileReferenceInFileHasTag
|
|
||||||
stmt.deleteFileReferenceInFileHasTag, err = sqlConn.Prepare(
|
|
||||||
deleteFileReferenceInFileHasTagQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init deleteFileReferenceInReviews
|
|
||||||
stmt.deleteFileReferenceInReviews, err = sqlConn.Prepare(
|
|
||||||
deleteFileReferenceInReviewsQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init updateFilename
|
|
||||||
stmt.updateFilename, err = sqlConn.Prepare(updateFilenameQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// init resetFilename
|
|
||||||
stmt.resetFilename, err = sqlConn.Prepare(resetFilenameQuery)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return stmt, err
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
type File struct {
|
|
||||||
Db *Database `json:"-"`
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Folder_id int64 `json:"folder_id"`
|
|
||||||
Foldername string `json:"foldername"`
|
|
||||||
Realname string `json:"-"`
|
|
||||||
Filename string `json:"filename"`
|
|
||||||
Filesize int64 `json:"filesize"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Folder struct {
|
|
||||||
Db *Database `json:"-"`
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Folder string `json:"-"`
|
|
||||||
Foldername string `json:"foldername"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type User struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Username string `json:"username"`
|
|
||||||
Password string `json:"-"`
|
|
||||||
Role int64 `json:"role"`
|
|
||||||
Active bool `json:"active"`
|
|
||||||
AvatarId int64 `json:"avatar_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Tag struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
CreatedByUserId int64 `json:"created_by_user_id"`
|
|
||||||
CreatedByUser *User `json:"created_by_user"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Review struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
FileId int64 `json:"file_id"`
|
|
||||||
File *File `json:"file"`
|
|
||||||
UserId int64 `json:"user_id"`
|
|
||||||
User *User `json:"user"`
|
|
||||||
CreatedAt int64 `json:"created_at"`
|
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
|
||||||
Content string `json:"content"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Feedback struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
UserId int64 `json:"user_id"`
|
|
||||||
User *User `json:"user"`
|
|
||||||
Content string `json:"content"`
|
|
||||||
Header string `json:"header"`
|
|
||||||
Time int64 `json:"time"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
RoleAnonymous = int64(0)
|
|
||||||
RoleAdmin = int64(1)
|
|
||||||
RoleUser = int64(2)
|
|
||||||
)
|
|
||||||
|
|
||||||
func (f *File) Path() (string, error) {
|
|
||||||
folder, err := f.Db.GetFolder(f.Folder_id)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return filepath.Join(folder.Folder, f.Realname), nil
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
package tmpfs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"msw-open-music/pkg/commonconfig"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Tmpfs struct {
|
|
||||||
record map[string]int64
|
|
||||||
Config commonconfig.TmpfsConfig
|
|
||||||
wg sync.WaitGroup
|
|
||||||
recordLocks map[string]*sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tmpfs *Tmpfs) GetObjFilePath(id int64, ffmpegConfig commonconfig.FfmpegConfig) string {
|
|
||||||
return filepath.Join(tmpfs.Config.Root, strconv.FormatInt(id, 10)+"."+ffmpegConfig.Name+"."+ffmpegConfig.Format)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tmpfs *Tmpfs) GetLock(filename string) *sync.Mutex {
|
|
||||||
if _, ok := tmpfs.recordLocks[filename]; !ok {
|
|
||||||
tmpfs.recordLocks[filename] = &sync.Mutex{}
|
|
||||||
}
|
|
||||||
return tmpfs.recordLocks[filename]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tmpfs *Tmpfs) Lock(filename string) {
|
|
||||||
tmpfs.GetLock(filename).Lock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tmpfs *Tmpfs) Unlock(filename string) {
|
|
||||||
tmpfs.GetLock(filename).Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTmpfs(config commonconfig.TmpfsConfig) *Tmpfs {
|
|
||||||
tmpfs := &Tmpfs{
|
|
||||||
record: make(map[string]int64),
|
|
||||||
Config: config,
|
|
||||||
recordLocks: make(map[string]*sync.Mutex),
|
|
||||||
}
|
|
||||||
// check if the directory exists
|
|
||||||
if _, err := os.Stat(tmpfs.Config.Root); os.IsNotExist(err) {
|
|
||||||
err = os.MkdirAll(tmpfs.Config.Root, 0755)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln("[tmpfs] Failed to create directory", tmpfs.Config.Root)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tmpfs.wg.Add(1)
|
|
||||||
go tmpfs.Cleaner()
|
|
||||||
return tmpfs
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tmpfs *Tmpfs) Record(filename string) {
|
|
||||||
tmpfs.record[filename] = time.Now().Unix()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tmpfs *Tmpfs) Exits(filename string) bool {
|
|
||||||
_, ok := tmpfs.record[filename]
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tmpfs *Tmpfs) Cleaner() {
|
|
||||||
var err error
|
|
||||||
for {
|
|
||||||
now := time.Now().Unix()
|
|
||||||
for path, lock := range tmpfs.recordLocks {
|
|
||||||
lock.Lock()
|
|
||||||
recordTime, ok := tmpfs.record[path]
|
|
||||||
if !ok {
|
|
||||||
lock.Unlock()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if now-recordTime > tmpfs.Config.FileLifeTime {
|
|
||||||
err = os.Remove(path)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("[tmpfs] Failed to remove file", err)
|
|
||||||
}
|
|
||||||
log.Println("[tmpfs] Deleted file", path)
|
|
||||||
delete(tmpfs.record, path)
|
|
||||||
delete(tmpfs.recordLocks, path)
|
|
||||||
}
|
|
||||||
lock.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,39 @@
|
|||||||
# Getting Started with Create React App
|
# msw-open-music web font-end
|
||||||
|
|
||||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
This msw-open-music project was bootstrapped with `Create React App`
|
||||||
|
|
||||||
|
## Group 9 information
|
||||||
|
|
||||||
|
| Name | Name (EN) | No |
|
||||||
|
| ------ | ------------- | ---------- |
|
||||||
|
| 陈永源 | CHEN Yongyuan | 1930006025 |
|
||||||
|
| 鲁雷 | Lu Lei | 2030026101 |
|
||||||
|
| 张滨玮 | Zhang Binwei | 2030026197 |
|
||||||
|
| 丁俊超 | Ding Junchao | 2030026258 |
|
||||||
|
| 邱星越 | Qiu Xingyue | 2030026119 |
|
||||||
|
| 李真晔 | Li Zhenye | 2030006104 |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## URL References
|
||||||
|
|
||||||
|
- `/#/` Default home page. Generate random files.
|
||||||
|
- `/#/search-files` Search files
|
||||||
|
- `/#/search-folders` Search folders
|
||||||
|
- `/#/folder/:id` Show files in the folder
|
||||||
|
- `/#/file/:id` Show file's information
|
||||||
|
- `/#/file/:id/share` Share a specific file
|
||||||
|
- `/#/file/:id/review` Review a file
|
||||||
|
- `/#/manage` Manage system setting and status
|
||||||
|
- `/#/login` Login
|
||||||
|
- `/#/register/` Register
|
||||||
|
- `/#/profile/:id` Profile of a user
|
||||||
|
|
||||||
|
## HOW TO DEPLOY?
|
||||||
|
|
||||||
|
Welcome to visit the demo <https://demo.uicbbs.com>
|
||||||
|
|
||||||
|
Put the files under `build` folder to your HTTP server (Apache, nginx, caddy, etc.) root folder.
|
||||||
|
|
||||||
## Available Scripts
|
## Available Scripts
|
||||||
|
|
||||||
@@ -14,11 +47,6 @@ Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
|||||||
The page will reload if you make edits.\
|
The page will reload if you make edits.\
|
||||||
You will also see any lint errors in the console.
|
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`
|
### `npm run build`
|
||||||
|
|
||||||
Builds the app for production to the `build` folder.\
|
Builds the app for production to the `build` folder.\
|
||||||
@@ -28,43 +56,3 @@ The build is minified and the filenames include the hashes.\
|
|||||||
Your app is ready to be deployed!
|
Your app is ready to be deployed!
|
||||||
|
|
||||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
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)
|
|
||||||
|
|||||||
111
web/package-lock.json
generated
111
web/package-lock.json
generated
@@ -13,9 +13,9 @@
|
|||||||
"@testing-library/user-event": "^12.8.3",
|
"@testing-library/user-event": "^12.8.3",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-router": "^6.3.0",
|
"react-router": "^6.0.2",
|
||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.0.2",
|
||||||
"react-scripts": "^4.0.3",
|
"react-scripts": "4.0.3",
|
||||||
"water.css": "^2.1.1",
|
"water.css": "^2.1.1",
|
||||||
"web-vitals": "^1.1.2"
|
"web-vitals": "^1.1.2"
|
||||||
},
|
},
|
||||||
@@ -5687,9 +5687,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001352",
|
"version": "1.0.30001280",
|
||||||
"resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001352.tgz",
|
"resolved": "https://registry.npmmirror.com/caniuse-lite/download/caniuse-lite-1.0.30001280.tgz?cache=0&sync_timestamp=1636700110747&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fcaniuse-lite%2Fdownload%2Fcaniuse-lite-1.0.30001280.tgz",
|
||||||
"integrity": "sha512-GUgH8w6YergqPQDGWhJGt8GDRnY0L/iJVQcU3eJ46GYf52R8tk0Wxp0PymuFVZboJYXGiCqwozAYZNRjVj6IcA=="
|
"integrity": "sha512-kFXwYvHe5rix25uwueBxC569o53J6TpnGu0BEEn+6Lhl2vsnAumRFWEBhDft1fwyo6m1r4i+RqA4+163FpeFcA==",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/browserslist"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/capture-exit": {
|
"node_modules/capture-exit": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@@ -9668,9 +9672,10 @@
|
|||||||
"integrity": "sha1-TAb8y0YC/iYCs8k9+C1+fb8aio4="
|
"integrity": "sha1-TAb8y0YC/iYCs8k9+C1+fb8aio4="
|
||||||
},
|
},
|
||||||
"node_modules/history": {
|
"node_modules/history": {
|
||||||
"version": "5.3.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/history/-/history-5.3.0.tgz",
|
"resolved": "https://registry.npmmirror.com/history/download/history-5.1.0.tgz",
|
||||||
"integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==",
|
"integrity": "sha1-LpPAnAZBlNONUu1ir9CvydmwHs4=",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.7.6"
|
"@babel/runtime": "^7.7.6"
|
||||||
}
|
}
|
||||||
@@ -15702,8 +15707,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/react/-/react-17.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/react/download/react-17.0.2.tgz?cache=0&sync_timestamp=1636647716894&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Freact%2Fdownload%2Freact-17.0.2.tgz",
|
||||||
"integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
|
"integrity": "sha1-0LXMUW0p6z7uOD91tihkz7aAADc=",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"object-assign": "^4.1.1"
|
"object-assign": "^4.1.1"
|
||||||
@@ -15843,8 +15848,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-17.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/react-dom/download/react-dom-17.0.2.tgz",
|
||||||
"integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==",
|
"integrity": "sha1-7P+2hF462Nv83EmPDQqTlzZQLCM=",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"object-assign": "^4.1.1",
|
"object-assign": "^4.1.1",
|
||||||
@@ -15873,23 +15878,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router": {
|
"node_modules/react-router": {
|
||||||
"version": "6.3.0",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/react-router/-/react-router-6.3.0.tgz",
|
"resolved": "https://registry.npmmirror.com/react-router/download/react-router-6.0.2.tgz",
|
||||||
"integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==",
|
"integrity": "sha512-8/Wm3Ed8t7TuedXjAvV39+c8j0vwrI5qVsYqjFr5WkJjsJpEvNSoLRUbtqSEYzqaTUj1IV+sbPJxvO+accvU0Q==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"history": "^5.2.0"
|
"history": "^5.1.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": ">=16.8"
|
"react": ">=16.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router-dom": {
|
"node_modules/react-router-dom": {
|
||||||
"version": "6.3.0",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-6.3.0.tgz",
|
"resolved": "https://registry.npmmirror.com/react-router-dom/download/react-router-dom-6.0.2.tgz?cache=0&sync_timestamp=1636500332816&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Freact-router-dom%2Fdownload%2Freact-router-dom-6.0.2.tgz",
|
||||||
"integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==",
|
"integrity": "sha512-cOpJ4B6raFutr0EG8O/M2fEoyQmwvZWomf1c6W2YXBZuFBx8oTk/zqjXghwScyhfrtnt0lANXV2182NQblRxFA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"history": "^5.2.0",
|
"history": "^5.1.0",
|
||||||
"react-router": "6.3.0"
|
"react-router": "6.0.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": ">=16.8",
|
"react": ">=16.8",
|
||||||
@@ -15898,8 +15903,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/react-scripts": {
|
"node_modules/react-scripts": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmmirror.com/react-scripts/-/react-scripts-4.0.3.tgz",
|
"resolved": "https://registry.npmmirror.com/react-scripts/download/react-scripts-4.0.3.tgz",
|
||||||
"integrity": "sha512-S5eO4vjUzUisvkIPB7jVsKtuH2HhWcASREYWHAQ1FP5HyCv3xgn+wpILAEWkmy+A+tTNbSZClhxjT3qz6g4L1A==",
|
"integrity": "sha1-scr+18P6YD52KLoPGHeHlky100U=",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "7.12.3",
|
"@babel/core": "7.12.3",
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": "0.4.3",
|
"@pmmmwh/react-refresh-webpack-plugin": "0.4.3",
|
||||||
@@ -19440,8 +19445,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/water.css": {
|
"node_modules/water.css": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmmirror.com/water.css/-/water.css-2.1.1.tgz",
|
"resolved": "https://registry.npmmirror.com/water.css/download/water.css-2.1.1.tgz",
|
||||||
"integrity": "sha512-gkO5byC+pZ7ndEV18hs/RmxKoDtEZXx06tZU4ocI3IBdv4xV64tlhjIFbDjurysRnNkiy2oQTr8PakRyzZWPJw=="
|
"integrity": "sha1-7m/oM6MTo6LttYilftYrPjHe3Rg="
|
||||||
},
|
},
|
||||||
"node_modules/wbuf": {
|
"node_modules/wbuf": {
|
||||||
"version": "1.7.3",
|
"version": "1.7.3",
|
||||||
@@ -19453,8 +19458,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/web-vitals": {
|
"node_modules/web-vitals": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmmirror.com/web-vitals/-/web-vitals-1.1.2.tgz",
|
"resolved": "https://registry.npmmirror.com/web-vitals/download/web-vitals-1.1.2.tgz",
|
||||||
"integrity": "sha512-PFMKIY+bRSXlMxVAQ+m2aw9c/ioUYfDgrYot0YUa+/xa0sakubWhSDyxAKwzymvXVdF4CZI71g06W+mqhzu6ig=="
|
"integrity": "sha1-BlNTCBaJhgliOaqEcW5otMaubRw="
|
||||||
},
|
},
|
||||||
"node_modules/webidl-conversions": {
|
"node_modules/webidl-conversions": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
@@ -25222,9 +25227,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"caniuse-lite": {
|
"caniuse-lite": {
|
||||||
"version": "1.0.30001352",
|
"version": "1.0.30001280",
|
||||||
"resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001352.tgz",
|
"resolved": "https://registry.npmmirror.com/caniuse-lite/download/caniuse-lite-1.0.30001280.tgz?cache=0&sync_timestamp=1636700110747&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Fcaniuse-lite%2Fdownload%2Fcaniuse-lite-1.0.30001280.tgz",
|
||||||
"integrity": "sha512-GUgH8w6YergqPQDGWhJGt8GDRnY0L/iJVQcU3eJ46GYf52R8tk0Wxp0PymuFVZboJYXGiCqwozAYZNRjVj6IcA=="
|
"integrity": "sha512-kFXwYvHe5rix25uwueBxC569o53J6TpnGu0BEEn+6Lhl2vsnAumRFWEBhDft1fwyo6m1r4i+RqA4+163FpeFcA=="
|
||||||
},
|
},
|
||||||
"capture-exit": {
|
"capture-exit": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@@ -28376,9 +28381,9 @@
|
|||||||
"integrity": "sha1-TAb8y0YC/iYCs8k9+C1+fb8aio4="
|
"integrity": "sha1-TAb8y0YC/iYCs8k9+C1+fb8aio4="
|
||||||
},
|
},
|
||||||
"history": {
|
"history": {
|
||||||
"version": "5.3.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/history/-/history-5.3.0.tgz",
|
"resolved": "https://registry.npmmirror.com/history/download/history-5.1.0.tgz",
|
||||||
"integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==",
|
"integrity": "sha1-LpPAnAZBlNONUu1ir9CvydmwHs4=",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/runtime": "^7.7.6"
|
"@babel/runtime": "^7.7.6"
|
||||||
}
|
}
|
||||||
@@ -33183,8 +33188,8 @@
|
|||||||
},
|
},
|
||||||
"react": {
|
"react": {
|
||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/react/-/react-17.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/react/download/react-17.0.2.tgz?cache=0&sync_timestamp=1636647716894&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Freact%2Fdownload%2Freact-17.0.2.tgz",
|
||||||
"integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
|
"integrity": "sha1-0LXMUW0p6z7uOD91tihkz7aAADc=",
|
||||||
"requires": {
|
"requires": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"object-assign": "^4.1.1"
|
"object-assign": "^4.1.1"
|
||||||
@@ -33298,8 +33303,8 @@
|
|||||||
},
|
},
|
||||||
"react-dom": {
|
"react-dom": {
|
||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-17.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/react-dom/download/react-dom-17.0.2.tgz",
|
||||||
"integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==",
|
"integrity": "sha1-7P+2hF462Nv83EmPDQqTlzZQLCM=",
|
||||||
"requires": {
|
"requires": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"object-assign": "^4.1.1",
|
"object-assign": "^4.1.1",
|
||||||
@@ -33322,26 +33327,26 @@
|
|||||||
"integrity": "sha1-ch1GV2ctQAxePHXQY8SoX7LV1o8="
|
"integrity": "sha1-ch1GV2ctQAxePHXQY8SoX7LV1o8="
|
||||||
},
|
},
|
||||||
"react-router": {
|
"react-router": {
|
||||||
"version": "6.3.0",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/react-router/-/react-router-6.3.0.tgz",
|
"resolved": "https://registry.npmmirror.com/react-router/download/react-router-6.0.2.tgz",
|
||||||
"integrity": "sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ==",
|
"integrity": "sha512-8/Wm3Ed8t7TuedXjAvV39+c8j0vwrI5qVsYqjFr5WkJjsJpEvNSoLRUbtqSEYzqaTUj1IV+sbPJxvO+accvU0Q==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"history": "^5.2.0"
|
"history": "^5.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-router-dom": {
|
"react-router-dom": {
|
||||||
"version": "6.3.0",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-6.3.0.tgz",
|
"resolved": "https://registry.npmmirror.com/react-router-dom/download/react-router-dom-6.0.2.tgz?cache=0&sync_timestamp=1636500332816&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2Freact-router-dom%2Fdownload%2Freact-router-dom-6.0.2.tgz",
|
||||||
"integrity": "sha512-uaJj7LKytRxZNQV8+RbzJWnJ8K2nPsOOEuX7aQstlMZKQT0164C+X2w6bnkqU3sjtLvpd5ojrezAyfZ1+0sStw==",
|
"integrity": "sha512-cOpJ4B6raFutr0EG8O/M2fEoyQmwvZWomf1c6W2YXBZuFBx8oTk/zqjXghwScyhfrtnt0lANXV2182NQblRxFA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"history": "^5.2.0",
|
"history": "^5.1.0",
|
||||||
"react-router": "6.3.0"
|
"react-router": "6.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-scripts": {
|
"react-scripts": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmmirror.com/react-scripts/-/react-scripts-4.0.3.tgz",
|
"resolved": "https://registry.npmmirror.com/react-scripts/download/react-scripts-4.0.3.tgz",
|
||||||
"integrity": "sha512-S5eO4vjUzUisvkIPB7jVsKtuH2HhWcASREYWHAQ1FP5HyCv3xgn+wpILAEWkmy+A+tTNbSZClhxjT3qz6g4L1A==",
|
"integrity": "sha1-scr+18P6YD52KLoPGHeHlky100U=",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/core": "7.12.3",
|
"@babel/core": "7.12.3",
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": "0.4.3",
|
"@pmmmwh/react-refresh-webpack-plugin": "0.4.3",
|
||||||
@@ -36252,8 +36257,8 @@
|
|||||||
},
|
},
|
||||||
"water.css": {
|
"water.css": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmmirror.com/water.css/-/water.css-2.1.1.tgz",
|
"resolved": "https://registry.npmmirror.com/water.css/download/water.css-2.1.1.tgz",
|
||||||
"integrity": "sha512-gkO5byC+pZ7ndEV18hs/RmxKoDtEZXx06tZU4ocI3IBdv4xV64tlhjIFbDjurysRnNkiy2oQTr8PakRyzZWPJw=="
|
"integrity": "sha1-7m/oM6MTo6LttYilftYrPjHe3Rg="
|
||||||
},
|
},
|
||||||
"wbuf": {
|
"wbuf": {
|
||||||
"version": "1.7.3",
|
"version": "1.7.3",
|
||||||
@@ -36265,8 +36270,8 @@
|
|||||||
},
|
},
|
||||||
"web-vitals": {
|
"web-vitals": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmmirror.com/web-vitals/-/web-vitals-1.1.2.tgz",
|
"resolved": "https://registry.npmmirror.com/web-vitals/download/web-vitals-1.1.2.tgz",
|
||||||
"integrity": "sha512-PFMKIY+bRSXlMxVAQ+m2aw9c/ioUYfDgrYot0YUa+/xa0sakubWhSDyxAKwzymvXVdF4CZI71g06W+mqhzu6ig=="
|
"integrity": "sha1-BlNTCBaJhgliOaqEcW5otMaubRw="
|
||||||
},
|
},
|
||||||
"webidl-conversions": {
|
"webidl-conversions": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "msw-open-music-react",
|
"name": "msw-open-music-react",
|
||||||
"version": "1.2.0",
|
"version": "1.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@testing-library/jest-dom": "^5.15.0",
|
"@testing-library/jest-dom": "^5.15.0",
|
||||||
@@ -8,9 +8,9 @@
|
|||||||
"@testing-library/user-event": "^12.8.3",
|
"@testing-library/user-event": "^12.8.3",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-router": "^6.3.0",
|
"react-router": "^6.0.2",
|
||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.0.2",
|
||||||
"react-scripts": "^4.0.3",
|
"react-scripts": "4.0.3",
|
||||||
"water.css": "^2.1.1",
|
"water.css": "^2.1.1",
|
||||||
"web-vitals": "^1.1.2"
|
"web-vitals": "^1.1.2"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,13 +15,18 @@ body {
|
|||||||
box-shadow: 0 0 8px #393939;
|
box-shadow: 0 0 8px #393939;
|
||||||
border-radius: 6px 6px 0 0;
|
border-radius: 6px 6px 0 0;
|
||||||
}
|
}
|
||||||
|
.avatar {
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: lightpink;
|
||||||
|
padding: 0.39rem;
|
||||||
|
}
|
||||||
.title {
|
.title {
|
||||||
margin-left: 1em;
|
margin-left: 1em;
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
vertical-align: middle;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
color: white;
|
|
||||||
}
|
}
|
||||||
.title-text {
|
.title-text {
|
||||||
margin-left: 1em;
|
margin-left: 1em;
|
||||||
@@ -95,19 +100,3 @@ 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;
|
|
||||||
}
|
|
||||||
|
|||||||
198
web/src/App.js
198
web/src/App.js
@@ -4,150 +4,86 @@ import "./App.css";
|
|||||||
import GetRandomFiles from "./component/GetRandomFiles";
|
import GetRandomFiles from "./component/GetRandomFiles";
|
||||||
import SearchFiles from "./component/SearchFiles";
|
import SearchFiles from "./component/SearchFiles";
|
||||||
import SearchFolders from "./component/SearchFolders";
|
import SearchFolders from "./component/SearchFolders";
|
||||||
import FilesInFolder from "./component/FilesInFolder";
|
|
||||||
import Manage from "./component/Manage";
|
import Manage from "./component/Manage";
|
||||||
import ManageUser from "./component/ManageUser";
|
|
||||||
import FileInfo from "./component/FileInfo";
|
|
||||||
import Share from "./component/Share";
|
import Share from "./component/Share";
|
||||||
|
import AudioPlayer from "./component/AudioPlayer";
|
||||||
|
import FilesInFolder from "./component/FilesInFolder";
|
||||||
|
import FileInfo from "./component/FileInfo";
|
||||||
|
import Review from "./component/Review";
|
||||||
|
import Profile from "./component/Profile";
|
||||||
|
import User from "./component/User";
|
||||||
import Login from "./component/Login";
|
import Login from "./component/Login";
|
||||||
import Register from "./component/Register";
|
import Register from "./component/Register";
|
||||||
import Tags from "./component/Tags";
|
import { useState } from "react";
|
||||||
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() {
|
function App() {
|
||||||
const [playingFile, setPlayingFile] = useState({});
|
const [playingFile, setPlayingFile] = useState({});
|
||||||
const [user, setUser] = useState({});
|
const [user, setUser] = useState(null);
|
||||||
const [langCode, setLangCode] = useState("en_US");
|
|
||||||
|
|
||||||
// select language
|
|
||||||
useEffect(() => {
|
|
||||||
const browserCode = window.navigator.language;
|
|
||||||
for (const key in LANG_OPTIONS) {
|
|
||||||
for (const i in LANG_OPTIONS[key].matches) {
|
|
||||||
const code = LANG_OPTIONS[key].matches[i];
|
|
||||||
if (code === browserCode) {
|
|
||||||
setLangCode(key);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// fallback to english
|
|
||||||
setLangCode('en-US');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="base">
|
<div className="base">
|
||||||
<langCodeContext.Provider value={{ langCode, setLangCode }}>
|
<Router>
|
||||||
<Router>
|
<header className="header">
|
||||||
<header className="header">
|
<h3 className="title">
|
||||||
<h3 className="title">
|
<img src="favicon.png" alt="logo" className="logo" />
|
||||||
<img src="favicon.png" alt="logo" className="logo" />
|
<span className="title-text">MSW Open Music Project</span>
|
||||||
<span className="title-text">MSW Open Music Project</span>
|
<User user={user} setUser={setUser} />
|
||||||
<UserStatus user={user} setUser={setUser} />
|
</h3>
|
||||||
</h3>
|
<nav className="nav">
|
||||||
<nav className="nav">
|
<NavLink to="/" className="nav-link">
|
||||||
<NavLink to="/" className="nav-link">
|
Feeling luckly
|
||||||
{Tr("Feeling luckly")}
|
</NavLink>
|
||||||
</NavLink>
|
<NavLink to="/search-files" className="nav-link">
|
||||||
<NavLink to="/files" className="nav-link">
|
Files
|
||||||
{Tr("Files")}
|
</NavLink>
|
||||||
</NavLink>
|
<NavLink to="/search-folders" className="nav-link">
|
||||||
<NavLink to="/folders" className="nav-link">
|
Folders
|
||||||
{Tr("Folders")}
|
</NavLink>
|
||||||
</NavLink>
|
<NavLink to="/manage" className="nav-link">
|
||||||
<NavLink to="/manage" className="nav-link">
|
Manage
|
||||||
{Tr("Manage")}
|
</NavLink>
|
||||||
</NavLink>
|
</nav>
|
||||||
</nav>
|
</header>
|
||||||
</header>
|
<main>
|
||||||
<main>
|
<Routes>
|
||||||
<Routes>
|
<Route
|
||||||
<Route
|
index
|
||||||
index
|
path="/"
|
||||||
path="/"
|
element={<GetRandomFiles setPlayingFile={setPlayingFile} />}
|
||||||
element={<GetRandomFiles setPlayingFile={setPlayingFile} />}
|
/>
|
||||||
/>
|
<Route
|
||||||
<Route
|
path="/search-files"
|
||||||
path="/files"
|
element={<SearchFiles setPlayingFile={setPlayingFile} />}
|
||||||
element={<SearchFiles setPlayingFile={setPlayingFile} />}
|
/>
|
||||||
/>
|
<Route
|
||||||
<Route
|
path="/search-folders"
|
||||||
path="/folders"
|
element={<SearchFolders setPlayingFile={setPlayingFile} />}
|
||||||
element={<SearchFolders setPlayingFile={setPlayingFile} />}
|
/>
|
||||||
/>
|
<Route
|
||||||
<Route
|
path="/folder/:id"
|
||||||
path="/folders/:id"
|
element={<FilesInFolder setPlayingFile={setPlayingFile} />}
|
||||||
element={<FilesInFolder setPlayingFile={setPlayingFile} />}
|
/>
|
||||||
/>
|
<Route path="/manage" element={<Manage />} />
|
||||||
<Route
|
<Route
|
||||||
path="/manage"
|
path="/file/:id/share"
|
||||||
element={
|
element={<Share setPlayingFile={setPlayingFile} />}
|
||||||
<Manage
|
/>
|
||||||
user={user}
|
<Route path="/file/:id/review" element={<Review />} />
|
||||||
setUser={setUser}
|
<Route
|
||||||
setLangCode={setLangCode}
|
path="/profile/:id"
|
||||||
/>
|
element={<Profile user={user} setUser={setUser} />}
|
||||||
}
|
/>
|
||||||
/>
|
<Route path="/login" element={<Login setUser={setUser} />} />
|
||||||
<Route
|
<Route path="/register" element={<Register setUser={setUser} />} />
|
||||||
path="/manage/feedbacks"
|
<Route path="/file/:id" element={<FileInfo />} />
|
||||||
element={<FeedbackPage user={user} />}
|
</Routes>
|
||||||
/>
|
</main>
|
||||||
<Route
|
<footer>
|
||||||
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
|
<AudioPlayer
|
||||||
playingFile={playingFile}
|
playingFile={playingFile}
|
||||||
setPlayingFile={setPlayingFile}
|
setPlayingFile={setPlayingFile}
|
||||||
/>
|
/>
|
||||||
</Router>
|
</footer>
|
||||||
</langCodeContext.Provider>
|
</Router>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useNavigate } from "react-router";
|
|||||||
import { CalcReadableFilesizeDetail } from "./Common";
|
import { CalcReadableFilesizeDetail } from "./Common";
|
||||||
import FfmpegConfig from "./FfmpegConfig";
|
import FfmpegConfig from "./FfmpegConfig";
|
||||||
import FileDialog from "./FileDialog";
|
import FileDialog from "./FileDialog";
|
||||||
import { Tr } from "../translate";
|
|
||||||
|
|
||||||
function AudioPlayer(props) {
|
function AudioPlayer(props) {
|
||||||
// props.playingFile
|
// props.playingFile
|
||||||
@@ -13,14 +12,10 @@ function AudioPlayer(props) {
|
|||||||
const [loop, setLoop] = useState(true);
|
const [loop, setLoop] = useState(true);
|
||||||
const [raw, setRaw] = useState(false);
|
const [raw, setRaw] = useState(false);
|
||||||
const [prepare, setPrepare] = useState(false);
|
const [prepare, setPrepare] = useState(false);
|
||||||
const [selectedFfmpegConfig, setSelectedFfmpegConfig] = useState({
|
const [selectedFfmpegConfig, setSelectedFfmpegConfig] = useState({});
|
||||||
name: "",
|
|
||||||
args: "",
|
|
||||||
});
|
|
||||||
const [playingURL, setPlayingURL] = useState("");
|
const [playingURL, setPlayingURL] = useState("");
|
||||||
const [isPreparing, setIsPreparing] = useState(false);
|
const [isPreparing, setIsPreparing] = useState(false);
|
||||||
const [timerCount, setTimerCount] = useState(0);
|
const [preparedFilesize, setPreparedFilesize] = useState(null);
|
||||||
const [timerID, setTimerID] = useState(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// no playing file
|
// no playing file
|
||||||
@@ -45,12 +40,7 @@ function AudioPlayer(props) {
|
|||||||
})
|
})
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.error) {
|
setPreparedFilesize(data.filesize);
|
||||||
alert(data.error);
|
|
||||||
setIsPreparing(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
props.setPlayingFile(data.file);
|
|
||||||
setIsPreparing(false);
|
setIsPreparing(false);
|
||||||
setPlayingURL(
|
setPlayingURL(
|
||||||
`/api/v1/get_file_stream_direct?id=${props.playingFile.id}&config=${selectedFfmpegConfig.name}`
|
`/api/v1/get_file_stream_direct?id=${props.playingFile.id}&config=${selectedFfmpegConfig.name}`
|
||||||
@@ -67,8 +57,8 @@ function AudioPlayer(props) {
|
|||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="vertical">
|
<div>
|
||||||
<h5>{Tr("Player status")}</h5>
|
<h5>Player status</h5>
|
||||||
{props.playingFile.id && (
|
{props.playingFile.id && (
|
||||||
<span>
|
<span>
|
||||||
<FileDialog
|
<FileDialog
|
||||||
@@ -89,16 +79,20 @@ function AudioPlayer(props) {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate(`/folders/${props.playingFile.folder_id}`)}
|
onClick={() =>
|
||||||
|
navigate(`search-folders/${props.playingFile.folder_id}`)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{props.playingFile.foldername}
|
{props.playingFile.foldername}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button disabled>
|
<button disabled>
|
||||||
{CalcReadableFilesizeDetail(props.playingFile.filesize)}
|
{prepare
|
||||||
|
? CalcReadableFilesizeDetail(preparedFilesize)
|
||||||
|
: CalcReadableFilesizeDetail(props.playingFile.filesize)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isPreparing && <button disabled>{Tr("Preparing...")}</button>}
|
{isPreparing && <button disabled>Preparing...</button>}
|
||||||
|
|
||||||
{playingURL !== "" && (
|
{playingURL !== "" && (
|
||||||
<button
|
<button
|
||||||
@@ -106,7 +100,7 @@ function AudioPlayer(props) {
|
|||||||
props.setPlayingFile({});
|
props.setPlayingFile({});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Tr("Stop")}
|
Stop
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
@@ -114,69 +108,33 @@ function AudioPlayer(props) {
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<span className="horizontal">
|
<input
|
||||||
<input
|
checked={loop}
|
||||||
className="number-input"
|
onChange={(event) => setLoop(event.target.checked)}
|
||||||
disabled={timerID !== null}
|
type="checkbox"
|
||||||
type="number"
|
/>
|
||||||
value={timerCount}
|
<label>Loop</label>
|
||||||
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>
|
<input
|
||||||
|
checked={raw}
|
||||||
|
onChange={(event) => setRaw(event.target.checked)}
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<label>Raw</label>
|
||||||
|
|
||||||
|
{!raw && (
|
||||||
<span>
|
<span>
|
||||||
<input
|
<input
|
||||||
checked={loop}
|
checked={prepare}
|
||||||
onChange={(event) => setLoop(event.target.checked)}
|
onChange={(event) => setPrepare(event.target.checked)}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
/>
|
/>
|
||||||
<label>{Tr("Loop")}</label>
|
<label>Prepare</label>
|
||||||
</span>
|
</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>
|
|
||||||
|
|
||||||
{playingURL !== "" && (
|
{playingURL !== "" && (
|
||||||
<audio
|
<audio
|
||||||
id="dom-player"
|
|
||||||
controls
|
controls
|
||||||
autoPlay
|
autoPlay
|
||||||
loop={loop}
|
loop={loop}
|
||||||
@@ -189,7 +147,7 @@ function AudioPlayer(props) {
|
|||||||
selectedFfmpegConfig={selectedFfmpegConfig}
|
selectedFfmpegConfig={selectedFfmpegConfig}
|
||||||
setSelectedFfmpegConfig={setSelectedFfmpegConfig}
|
setSelectedFfmpegConfig={setSelectedFfmpegConfig}
|
||||||
/>
|
/>
|
||||||
</footer>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,3 @@
|
|||||||
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;
|
||||||
@@ -43,30 +35,6 @@ 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";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
import { useState, useEffect, useContext } from "react";
|
|
||||||
import { Tr, tr, langCodeContext } from "../translate";
|
|
||||||
|
|
||||||
function Database() {
|
|
||||||
const [walkPath, setWalkPath] = useState("");
|
|
||||||
const [patternString, setPatternString] = useState(
|
|
||||||
"wav flac mp3 ogg m4a mka"
|
|
||||||
);
|
|
||||||
const [tags, setTags] = useState([]);
|
|
||||||
const [selectedTags, setSelectedTags] = useState([]);
|
|
||||||
const [updating, setUpdating] = useState(false);
|
|
||||||
const { langCode } = useContext(langCodeContext);
|
|
||||||
|
|
||||||
function getTags() {
|
|
||||||
fetch("/api/v1/get_tags")
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((data) => {
|
|
||||||
if (data.error) {
|
|
||||||
alert(data.error);
|
|
||||||
} else {
|
|
||||||
setTags(data.tags);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getTags();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
function updateDatabase() {
|
|
||||||
// split pattern string into array
|
|
||||||
let patternArray = patternString.split(" ");
|
|
||||||
// remove whitespace from array
|
|
||||||
patternArray = patternArray.map((item) => item.trim());
|
|
||||||
// remove empty strings from array
|
|
||||||
patternArray = patternArray.filter((item) => item !== "");
|
|
||||||
// add dot before item array
|
|
||||||
patternArray = patternArray.map((item) => "." + item);
|
|
||||||
|
|
||||||
setUpdating(true);
|
|
||||||
|
|
||||||
fetch("/api/v1/walk", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
root: walkPath,
|
|
||||||
pattern: patternArray,
|
|
||||||
tag_ids: selectedTags,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((data) => {
|
|
||||||
if (data.error) {
|
|
||||||
alert(data.error);
|
|
||||||
} else {
|
|
||||||
alert("Database updated");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setUpdating(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h3>{Tr("Update Database")}</h3>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={walkPath}
|
|
||||||
placeholder={tr("walk path", langCode)}
|
|
||||||
onChange={(e) => setWalkPath(e.target.value)}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={patternString}
|
|
||||||
placeholder={tr("pattern wav flac mp3", langCode)}
|
|
||||||
onChange={(e) => setPatternString(e.target.value)}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<h4>{Tr("Tags")}</h4>
|
|
||||||
{tags.map((tag) => (
|
|
||||||
<div key={tag.id}>
|
|
||||||
<input
|
|
||||||
id={tag.id}
|
|
||||||
type="checkbox"
|
|
||||||
value={tag.id}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (e.target.checked) {
|
|
||||||
setSelectedTags([...selectedTags, tag.id]);
|
|
||||||
} else {
|
|
||||||
setSelectedTags(
|
|
||||||
selectedTags.filter((item) => item !== tag.id)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<label htmlFor={tag.id}>{tag.name}</label>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
updateDatabase();
|
|
||||||
}}
|
|
||||||
disabled={updating}
|
|
||||||
>
|
|
||||||
{updating ? Tr("Updating...") : Tr("Update Database")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Database;
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
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,4 +1,5 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import getFfmpegConfigListRespondExample from "../example-respond/get_ffmpeg_config_list.json"
|
||||||
|
|
||||||
function FfmpegConfig(props) {
|
function FfmpegConfig(props) {
|
||||||
// props.setSelectedFfmpegConfig
|
// props.setSelectedFfmpegConfig
|
||||||
@@ -7,17 +8,8 @@ function FfmpegConfig(props) {
|
|||||||
const [ffmpegConfigList, setFfmpegConfigList] = useState([]);
|
const [ffmpegConfigList, setFfmpegConfigList] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch("/api/v1/get_ffmpeg_config_list")
|
setFfmpegConfigList(getFfmpegConfigListRespondExample.ffmpeg_config_list);
|
||||||
.then((response) => response.json())
|
props.setSelectedFfmpegConfig(getFfmpegConfigListRespondExample.ffmpeg_config_list[0]);
|
||||||
.then((data) => {
|
|
||||||
setFfmpegConfigList(data.ffmpeg_config_list);
|
|
||||||
if (data.ffmpeg_config_list.length > 0) {
|
|
||||||
props.setSelectedFfmpegConfig(data.ffmpeg_config_list[0]);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
alert("get_ffmpeg_config_list error: " + error);
|
|
||||||
});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -33,7 +25,7 @@ function FfmpegConfig(props) {
|
|||||||
<option key={ffmpegConfig.name}>{ffmpegConfig.name}</option>
|
<option key={ffmpegConfig.name}>{ffmpegConfig.name}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<span className="warp-word">{props.selectedFfmpegConfig.args}</span>
|
<span>{props.selectedFfmpegConfig.args}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { Tr } from "../translate";
|
|
||||||
|
|
||||||
function FileDialog(props) {
|
function FileDialog(props) {
|
||||||
// props.showStatus
|
// props.showStatus
|
||||||
@@ -10,46 +9,33 @@ function FileDialog(props) {
|
|||||||
|
|
||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
|
|
||||||
|
const downloadURL = "/api/v1/get_file_direct?id=" + props.file.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<dialog
|
<dialog open={props.showStatus}>
|
||||||
open={props.showStatus}
|
<p>{props.file.filename}</p>
|
||||||
style={{
|
|
||||||
zIndex: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
cursor: "pointer",
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
props.setPlayingFile(props.file);
|
|
||||||
props.setShowStatus(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.file.filename}
|
|
||||||
</p>
|
|
||||||
<p>
|
<p>
|
||||||
{Tr("Play: play using browser player.")}
|
Download using browser
|
||||||
|
<br />
|
||||||
|
Play on the web page
|
||||||
<br />
|
<br />
|
||||||
{Tr("Info for more actions.")}
|
|
||||||
</p>
|
</p>
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
navigate(`/files/${props.file.id}`);
|
|
||||||
props.setShowStatus(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Tr("Info")}
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.setPlayingFile(props.file);
|
props.setPlayingFile(props.file);
|
||||||
props.setShowStatus(false);
|
props.setShowStatus(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Tr("Play")}
|
Play
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => props.setShowStatus(false)}>{Tr("Close")}</button>
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
navigate(`/file/${props.file.id}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Info
|
||||||
|
</button>
|
||||||
|
<button onClick={() => props.setShowStatus(false)}>Close</button>
|
||||||
</dialog>
|
</dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ function FileEntry(props) {
|
|||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className="clickable"
|
className="clickable"
|
||||||
onClick={() => navigate(`/folders/${props.file.folder_id}`)}
|
onClick={() => navigate(`/folder/${props.file.folder_id}`)}
|
||||||
>
|
>
|
||||||
{props.file.foldername}
|
{props.file.foldername}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -1,306 +1,67 @@
|
|||||||
import { useNavigate, useParams } from "react-router";
|
import { useParams, Link, useNavigate } from "react-router-dom";
|
||||||
import { useContext, useEffect, useState } from "react";
|
|
||||||
import { Tr, tr, langCodeContext } from "../translate";
|
|
||||||
|
|
||||||
function FileInfo(props) {
|
function FileInfo() {
|
||||||
let navigate = useNavigate();
|
|
||||||
let params = useParams();
|
let params = useParams();
|
||||||
const [file, setFile] = useState({
|
let navigate = useNavigate();
|
||||||
id: "",
|
|
||||||
folder_id: "",
|
|
||||||
foldername: "",
|
|
||||||
filename: "",
|
|
||||||
filesize: "",
|
|
||||||
});
|
|
||||||
const [tags, setTags] = useState([]);
|
|
||||||
const [tagsOnFile, setTagsOnFile] = useState([]);
|
|
||||||
const [selectedTagID, setSelectedTagID] = useState("");
|
|
||||||
const { langCode } = useContext(langCodeContext);
|
|
||||||
|
|
||||||
function refresh() {
|
|
||||||
fetch(`/api/v1/get_file_info`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
id: parseInt(params.id),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((data) => {
|
|
||||||
if (data.error) {
|
|
||||||
alert(data.error);
|
|
||||||
} else {
|
|
||||||
setFile(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTags() {
|
|
||||||
fetch(`/api/v1/get_tags`)
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((data) => {
|
|
||||||
if (data.error) {
|
|
||||||
alert(data.error);
|
|
||||||
} else {
|
|
||||||
setTags(data.tags);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTagsOnFile() {
|
|
||||||
fetch(`/api/v1/get_tags_on_file`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
id: parseInt(params.id),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((data) => {
|
|
||||||
if (data.error) {
|
|
||||||
alert(data.error);
|
|
||||||
} else {
|
|
||||||
setTagsOnFile(data.tags);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeTagOnFile(tag_id) {
|
|
||||||
fetch(`/api/v1/delete_tag_on_file`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
file_id: parseInt(params.id),
|
|
||||||
tag_id: tag_id,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((data) => {
|
|
||||||
if (data.error) {
|
|
||||||
alert(data.error);
|
|
||||||
} else {
|
|
||||||
getTagsOnFile();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteFile() {
|
|
||||||
// show Warning
|
|
||||||
if (
|
|
||||||
window.confirm(tr("Are you sure you want to delete this file?", langCode))
|
|
||||||
) {
|
|
||||||
fetch(`/api/v1/delete_file`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
id: parseInt(params.id),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((data) => {
|
|
||||||
if (data.error) {
|
|
||||||
alert(data.error);
|
|
||||||
} else {
|
|
||||||
navigate(-1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateFilename() {
|
|
||||||
fetch(`/api/v1/update_filename`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
id: parseInt(params.id),
|
|
||||||
filename: file.filename,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((data) => {
|
|
||||||
if (data.error) {
|
|
||||||
alert(data.error);
|
|
||||||
} else {
|
|
||||||
alert(tr("Filename updated", langCode));
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetFilename() {
|
|
||||||
fetch(`/api/v1/reset_filename`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
id: parseInt(params.id),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((data) => {
|
|
||||||
if (data.error) {
|
|
||||||
alert(data.error);
|
|
||||||
} else {
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
refresh();
|
|
||||||
getTags();
|
|
||||||
getTagsOnFile();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const downloadURL = "/api/v1/get_file_direct?id=" + file.id;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<h3>{Tr("File Details")}</h3>
|
<h3>File Information</h3>
|
||||||
<div>
|
<span>
|
||||||
<a href={downloadURL} download>
|
<button>Download</button>
|
||||||
<button>{Tr("Download")}</button>
|
|
||||||
</a>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.setPlayingFile(file);
|
navigate("/file/" + params.id + '/share');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Tr("Play")}
|
Share
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate(`/files/${params.id}/review`);
|
navigate("/file/" + params.id + '/review');
|
||||||
}}
|
}}
|
||||||
>
|
>Review</button>
|
||||||
{Tr("Review")}
|
</span>
|
||||||
</button>
|
<table>
|
||||||
<button
|
<thead>
|
||||||
onClick={() => {
|
<tr>
|
||||||
navigate(`/files/${params.id}/share`);
|
<th>Name</th>
|
||||||
}}
|
<th>Value</th>
|
||||||
>
|
</tr>
|
||||||
{Tr("Share")}
|
</thead>
|
||||||
</button>
|
<tbody>
|
||||||
<button
|
<tr>
|
||||||
onClick={() => {
|
<td>File Name</td>
|
||||||
deleteFile();
|
<td>{params.id}</td>
|
||||||
}}
|
</tr>
|
||||||
>
|
<tr>
|
||||||
{Tr("Delete")}
|
<td>File Size</td>
|
||||||
</button>
|
<td>123456</td>
|
||||||
</div>
|
</tr>
|
||||||
<div>
|
<tr>
|
||||||
<label htmlFor="foldername">{Tr("Folder Name")}</label>
|
<td>File Type</td>
|
||||||
<input
|
<td>media/aac</td>
|
||||||
type="text"
|
</tr>
|
||||||
id="foldername"
|
<tr>
|
||||||
value={file.foldername}
|
<td>Last Modified</td>
|
||||||
onClick={() => {
|
<td>2020-01-01</td>
|
||||||
navigate(`/folders/${file.folder_id}`);
|
</tr>
|
||||||
}}
|
<tr>
|
||||||
readOnly
|
<td>Import by</td>
|
||||||
/>
|
<td>
|
||||||
<label htmlFor="filename">{Tr("Filename")}</label>
|
<Link to="/profile/3">@admin</Link>
|
||||||
<input
|
</td>
|
||||||
type="text"
|
</tr>
|
||||||
id="filename"
|
<tr>
|
||||||
value={file.filename}
|
<td>Import Date</td>
|
||||||
onChange={(event) => {
|
<td>2020-01-01</td>
|
||||||
setFile({
|
</tr>
|
||||||
...file,
|
<tr>
|
||||||
filename: event.target.value,
|
<td>Location</td>
|
||||||
});
|
<td>/data/media/aac</td>
|
||||||
}}
|
</tr>
|
||||||
/>
|
</tbody>
|
||||||
<label htmlFor="filesize">{Tr("File size")}</label>
|
</table>
|
||||||
<input type="text" id="filesize" value={file.filesize} readOnly />
|
<button>Update</button>
|
||||||
</div>
|
|
||||||
<div className="horizontal">
|
|
||||||
<button onClick={updateFilename}>{Tr("Save")}</button>
|
|
||||||
<button onClick={resetFilename}>{Tr("Reset")}</button>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label>{Tr("Tags")}</label>
|
|
||||||
<ul>
|
|
||||||
{tagsOnFile.map((tag) => {
|
|
||||||
return (
|
|
||||||
<li key={tag.id}>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
navigate(`/manage/tags/${tag.id}`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tag.name}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
removeTagOnFile(tag.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Tr("Remove")}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
<div>
|
|
||||||
<select
|
|
||||||
onChange={(e) => {
|
|
||||||
setSelectedTagID(e.target.value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<option value="">{tr("Select a tag", langCode)}</option>
|
|
||||||
{tags.map((tag) => {
|
|
||||||
return (
|
|
||||||
<option key={tag.id} value={tag.id}>
|
|
||||||
{tag.name}
|
|
||||||
</option>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</select>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
// check empty
|
|
||||||
if (selectedTagID === "") {
|
|
||||||
alert(tr("Please select a tag", langCode));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fetch(`/api/v1/put_tag_on_file`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
file_id: parseInt(params.id),
|
|
||||||
tag_id: parseInt(selectedTagID),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((data) => {
|
|
||||||
if (data.error) {
|
|
||||||
alert(data.error);
|
|
||||||
} else {
|
|
||||||
getTagsOnFile();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Tr("Add tag")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,135 +1,13 @@
|
|||||||
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 FilesTable from "./FilesTable";
|
||||||
import { Tr } from "../translate";
|
import searchFilesRespondExample from "../example-respond/search_files.json"
|
||||||
|
import {useParams} from "react-router";
|
||||||
|
|
||||||
function FilesInFolder(props) {
|
function FilesInFolder(props) {
|
||||||
let params = useParams();
|
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 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);
|
|
||||||
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 (
|
return (
|
||||||
<div className="page">
|
<div>
|
||||||
<h3>{Tr("Files in Folder")}</h3>
|
<h3>Files in folder id {params.id}</h3>
|
||||||
<div className="search_toolbar">
|
<FilesTable setPlayingFile={props.setPlayingFile} files={searchFilesRespondExample.files} />
|
||||||
<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} />
|
|
||||||
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
import FileEntry from "./FileEntry";
|
import FileEntry from "./FileEntry";
|
||||||
import { Tr } from "../translate";
|
|
||||||
|
|
||||||
function FilesTable(props) {
|
function FilesTable(props) {
|
||||||
if (props.files.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{Tr("Filename")}</th>
|
<th>Filename</th>
|
||||||
<th>{Tr("Folder Name")}</th>
|
<th>Folder Name</th>
|
||||||
<th>{Tr("Size")}</th>
|
<th>Size</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -1,30 +1,26 @@
|
|||||||
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();
|
||||||
if (props.folders.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{Tr("Folder name")}</th>
|
<th>Folder name</th>
|
||||||
<th>{Tr("Action")}</th>
|
<th>Action</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{props.folders.map((folder) => (
|
{props.folders.map((folder) => (
|
||||||
<tr key={folder.id}>
|
<tr key={folder.id}>
|
||||||
<td
|
<td
|
||||||
onClick={() => navigate(`/folders/${folder.id}`)}
|
onClick={() => navigate(`/folder/${folder.id}`)}
|
||||||
className="clickable"
|
className="clickable"
|
||||||
>
|
>
|
||||||
{folder.foldername}
|
{folder.foldername}
|
||||||
</td>
|
</td>
|
||||||
<td onClick={() => navigate(`/folders/${folder.id}`)}>
|
<td onClick={() => navigate(`/folder/${folder.id}`)}>
|
||||||
<button>{Tr("View")}</button>
|
<button>View</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,107 +1,24 @@
|
|||||||
import { useContext, useEffect, useState } from "react";
|
import { useEffect, useState } 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";
|
import getRandomFilesRespondExample from "../example-respond/get_random_files.json"
|
||||||
|
|
||||||
function GetRandomFiles(props) {
|
function GetRandomFiles(props) {
|
||||||
const [files, setFiles] = useState([]);
|
const [files, setFiles] = useState([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [tags, setTags] = useState([]);
|
|
||||||
const navigator = useNavigate();
|
|
||||||
const query = useQuery();
|
|
||||||
const selectedTag = query.get("t") || "";
|
|
||||||
const { langCode } = useContext(langCodeContext);
|
|
||||||
|
|
||||||
function getRandomFiles() {
|
function refresh(setFiles) {
|
||||||
setIsLoading(true);
|
setFiles(getRandomFilesRespondExample.files);
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRandomFilesWithTag() {
|
|
||||||
setIsLoading(true);
|
|
||||||
fetch("/api/v1/get_random_files_with_tag", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
id: parseInt(selectedTag),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((data) => {
|
|
||||||
if (data.error) {
|
|
||||||
alert(data.error);
|
|
||||||
} else {
|
|
||||||
setFiles(data.files);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
alert("get_random_files_with_tag error: " + error);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function refresh() {
|
|
||||||
if (selectedTag === "") {
|
|
||||||
getRandomFiles();
|
|
||||||
} else {
|
|
||||||
getRandomFilesWithTag();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTags() {
|
|
||||||
fetch("/api/v1/get_tags")
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((data) => {
|
|
||||||
setTags(data.tags);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
alert("get_tags error: " + error);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getTags();
|
refresh(setFiles);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
refresh();
|
|
||||||
}, [selectedTag]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<div className="search_toolbar">
|
<div className="search_toolbar">
|
||||||
<button className="refresh" onClick={() => refresh(setFiles)}>
|
<button className="refresh" onClick={() => refresh(setFiles)}>
|
||||||
{isLoading ? Tr("Loading...") : Tr("Refresh")}
|
{isLoading ? "Loading..." : "Refresh"}
|
||||||
</button>
|
</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>
|
</div>
|
||||||
<FilesTable setPlayingFile={props.setPlayingFile} files={files} />
|
<FilesTable setPlayingFile={props.setPlayingFile} files={files} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,71 +1,45 @@
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useContext, useState } from "react";
|
import { useState } from "react";
|
||||||
import { Tr, tr, langCodeContext } from "../translate";
|
|
||||||
|
|
||||||
function Login(props) {
|
function Login(props) {
|
||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
let [username, setUsername] = useState("");
|
let [username, setUsername] = useState("");
|
||||||
let [password, setPassword] = 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 (
|
return (
|
||||||
<div className="page">
|
<div>
|
||||||
<h2>{Tr("Login")}</h2>
|
<h1>Login</h1>
|
||||||
<label htmlFor="username">{Tr("Username")}</label>
|
<label htmlFor="username"></label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="username"
|
id="username"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="password">{Tr("Password")}</label>
|
<label htmlFor="password">Password</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
id="password"
|
id="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
onKeyPress={(e) => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
e.preventDefault();
|
|
||||||
login();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
<button onClick={login}>{Tr("Login")}</button>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate("/manage/register");
|
if (!username || !password) {
|
||||||
|
alert("Please enter username and password");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
props.setUser({ id: 123, username: username, password: password });
|
||||||
|
navigate("/");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Tr("Register")}
|
Login
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
navigate("/register");
|
||||||
|
}}
|
||||||
|
>Register</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,98 +1,102 @@
|
|||||||
import { useNavigate } from "react-router";
|
import getFfmpegConfigListRespondExample from "../example-respond/get_ffmpeg_config_list.json";
|
||||||
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);
|
|
||||||
|
|
||||||
|
function Manage() {
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<h2>{Tr("Manage")}</h2>
|
<h2>Manage</h2>
|
||||||
<p>
|
<h3>Server status</h3>
|
||||||
{Tr("Hi")}, {props.user.username}
|
<table>
|
||||||
</p>
|
<thead>
|
||||||
|
<tr>
|
||||||
<select
|
<th>Name</th>
|
||||||
value={langCode}
|
<th>Value</th>
|
||||||
onChange={(event) => {
|
</tr>
|
||||||
setLangCode(event.target.value);
|
</thead>
|
||||||
}}
|
<tbody>
|
||||||
>
|
<tr>
|
||||||
{Object.keys(LANG_OPTIONS).map((code) => {
|
<td>Server status</td>
|
||||||
const langOption = LANG_OPTIONS[code];
|
<td>
|
||||||
return (
|
<span className="status-ok">OK</span>
|
||||||
<option value={code} key={code}>
|
</td>
|
||||||
{langOption.name}
|
</tr>
|
||||||
</option>
|
<tr>
|
||||||
);
|
<td>Server uptime</td>
|
||||||
})}
|
<td>
|
||||||
</select>
|
<span>1 day, 23 hours, 59 minutes and 59 seconds</span>
|
||||||
|
</td>
|
||||||
{props.user.role === 0 && (
|
</tr>
|
||||||
<div>
|
<tr>
|
||||||
<button
|
<td>Server load</td>
|
||||||
onClick={() => {
|
<td>
|
||||||
navigate("/manage/login");
|
<span>0.00 / 0.00 / 0.00</span>
|
||||||
}}
|
</td>
|
||||||
>
|
</tr>
|
||||||
{Tr("Login")}
|
<tr>
|
||||||
</button>
|
<td>Server memory usage</td>
|
||||||
<button
|
<td>
|
||||||
onClick={() => {
|
<span>0.00 MB</span>
|
||||||
navigate("/manage/register");
|
</td>
|
||||||
}}
|
</tr>
|
||||||
>
|
<tr>
|
||||||
{Tr("Register")}
|
<td>Server disk usage</td>
|
||||||
</button>
|
<td>
|
||||||
</div>
|
<span>0.00 MB</span>
|
||||||
)}
|
</td>
|
||||||
{props.user.role !== 0 && (
|
</tr>
|
||||||
<div className="horizontal">
|
<tr>
|
||||||
<button
|
<td>Server uptime</td>
|
||||||
onClick={() => {
|
<td>
|
||||||
navigate(`/manage/users/${props.user.id}`);
|
<span>1 day, 23 hours, 59 minutes and 59 seconds</span>
|
||||||
}}
|
</td>
|
||||||
>
|
</tr>
|
||||||
{Tr("Profile")}
|
<tr>
|
||||||
</button>
|
<td>Server load</td>
|
||||||
<button
|
<td>
|
||||||
onClick={() => {
|
<span>0.00 / 0.00 / 0.00</span>
|
||||||
fetch("/api/v1/logout")
|
</td>
|
||||||
.then((res) => res.json())
|
</tr>
|
||||||
.then((data) => {
|
<tr>
|
||||||
if (data.error) {
|
<td>Server memory usage</td>
|
||||||
alert(data.error);
|
<td>
|
||||||
} else {
|
<span>0.00 MB</span>
|
||||||
props.setUser(data.user);
|
</td>
|
||||||
}
|
</tr>
|
||||||
});
|
<tr>
|
||||||
}}
|
<td>Server disk usage</td>
|
||||||
>
|
<td>
|
||||||
{Tr("Logout")}
|
<span>0.00 MB</span>
|
||||||
</button>
|
</td>
|
||||||
</div>
|
</tr>
|
||||||
)}
|
</tbody>
|
||||||
<hr />
|
</table>
|
||||||
<div className="horizontal">
|
<h3>Database opeartions</h3>
|
||||||
<button onClick={() => navigate("/manage/tags")}>{Tr("Tags")}</button>
|
<ul>
|
||||||
<button onClick={() => navigate("/manage/users")}>{Tr("Users")}</button>
|
<li>.mp3</li>
|
||||||
<button onClick={() => navigate("/manage/feedbacks")}>
|
<li>.flac</li>
|
||||||
{Tr("Feedbacks")}
|
<li>.wav</li>
|
||||||
</button>
|
<li>.ogg</li>
|
||||||
</div>
|
<li>.aac</li>
|
||||||
<Database />
|
<li>.m4a</li>
|
||||||
<p>
|
</ul>
|
||||||
<a
|
<input type="text" placeholder=".mp3" />
|
||||||
href="https://github.com/heimoshuiyu/msw-open-music"
|
<button>Add Pattern</button>
|
||||||
target="_blank"
|
<input type="text" placeholder="/path/to/root" />
|
||||||
rel="noreferrer"
|
<button>Import</button>
|
||||||
>
|
<h3>Ffmpeg Settings</h3>
|
||||||
{Tr("View source code on Github")}
|
<ol>
|
||||||
</a>
|
{getFfmpegConfigListRespondExample.ffmpeg_config_list.map(
|
||||||
</p>
|
(item, index) => (
|
||||||
|
<li>
|
||||||
|
{item.name} {item.args}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</ol>
|
||||||
|
<span>
|
||||||
|
<input type="text" placeholder="name" />
|
||||||
|
<input type="text" placeholder="args" />
|
||||||
|
<button>Add</button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
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;
|
|
||||||
37
web/src/component/Profile.js
Normal file
37
web/src/component/Profile.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
|
import ReviewEntry from "./ReviewEntry";
|
||||||
|
import SearchFiles from "./SearchFiles";
|
||||||
|
import getRandomFilesRespondExample from "../example-respond/get_random_files.json";
|
||||||
|
|
||||||
|
function Profile(props) {
|
||||||
|
let params = useParams();
|
||||||
|
let navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Profile of user {params.id}</h1>
|
||||||
|
{props.user && props.user.id === parseInt(params.id) && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
props.setUser(null);
|
||||||
|
navigate("/");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<p>
|
||||||
|
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam
|
||||||
|
doloremque, quidem quisquam, quisquam quisquam quisquam quisquam
|
||||||
|
dignissimos.
|
||||||
|
</p>
|
||||||
|
<h3>Reviews</h3>
|
||||||
|
<ReviewEntry />
|
||||||
|
<ReviewEntry />
|
||||||
|
<h3>Liked music</h3>
|
||||||
|
<SearchFiles folder={{}} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Profile;
|
||||||
@@ -1,80 +1,49 @@
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useContext, useState } from "react";
|
import { useState } from "react";
|
||||||
import { tr, Tr, langCodeContext } from "../translate";
|
|
||||||
|
|
||||||
function Register() {
|
function Register(props) {
|
||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [password2, setPassword2] = 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 (
|
return (
|
||||||
<div className="page">
|
<div>
|
||||||
<h2>{Tr("Register")}</h2>
|
<h1>Register</h1>
|
||||||
<label htmlFor="username">{Tr("Username")}</label>
|
<label htmlFor="username">Username:</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="username"
|
id="username"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="password">{Tr("Password")}</label>
|
<label htmlFor="password">Password:</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
id="password"
|
id="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="password2">{Tr("Confirm Password")}</label>
|
<label htmlFor="password2">Confirm Password:</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
id="password2"
|
id="password2"
|
||||||
value={password2}
|
value={password2}
|
||||||
onChange={(e) => setPassword2(e.target.value)}
|
onChange={(e) => setPassword2(e.target.value)}
|
||||||
onKeyPress={(e) => {
|
/>
|
||||||
if (e.key === "Enter") {
|
<button
|
||||||
e.preventDefault();
|
onClick={() => {
|
||||||
register();
|
if (!username || !password || !password2) {
|
||||||
|
alert("Please fill out all fields");
|
||||||
|
} else if (password !== password2) {
|
||||||
|
alert("Passwords do not match");
|
||||||
|
} else {
|
||||||
|
props.setUser({ id: 39, username: username, password: password });
|
||||||
|
navigate("/");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
<label htmlFor="role">{Tr("Role")}</label>
|
Register
|
||||||
<select value={role} onChange={(e) => setRole(e.target.value)}>
|
</button>
|
||||||
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
35
web/src/component/Review.js
Normal file
35
web/src/component/Review.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { useParams } from "react-router";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import ReviewEntry from "./ReviewEntry";
|
||||||
|
|
||||||
|
function Review() {
|
||||||
|
let params = useParams();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page">
|
||||||
|
<h3>Review on music ID {params.id}</h3>
|
||||||
|
<textarea
|
||||||
|
className="review-text"
|
||||||
|
placeholder="Write your review here"
|
||||||
|
></textarea>
|
||||||
|
<span>
|
||||||
|
<button>Submit</button>
|
||||||
|
<button>Add to fav</button>
|
||||||
|
</span>
|
||||||
|
<details open>
|
||||||
|
<summary>Liked by</summary>
|
||||||
|
<p>
|
||||||
|
<Link to="/profile/1">@User 1</Link>
|
||||||
|
<Link to="/profile/2">@User 2</Link>
|
||||||
|
<Link to="/profile/3">@User 3</Link>
|
||||||
|
<Link to="/profile/4">@User 4</Link>
|
||||||
|
</p>
|
||||||
|
</details>
|
||||||
|
<ReviewEntry />
|
||||||
|
<ReviewEntry />
|
||||||
|
<ReviewEntry />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Review;
|
||||||
@@ -1,32 +1,17 @@
|
|||||||
import { Link } from "react-router-dom";
|
import { Link} from "react-router-dom";
|
||||||
import { convertIntToDateTime } from "./Common";
|
|
||||||
import { Tr, tr, langCodeContext } from "../translate";
|
|
||||||
import { useContext } from "react";
|
|
||||||
|
|
||||||
function ReviewEntry(props) {
|
function ReviewEntry() {
|
||||||
const { langCode } = useContext(langCodeContext);
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<p>
|
||||||
<h4>
|
<h5>
|
||||||
<Link to={`/manage/users/${props.review.user.id}`}>
|
<Link to="/profile/2">@rin</Link> comment music ID 39 at
|
||||||
@{props.review.user.username}
|
2019-01-01 12:23:45
|
||||||
</Link>{" "}
|
</h5>
|
||||||
{Tr("review")}{" "}
|
Agree with <Link to="/profile/1">@hmsy</Link>. I also like how well the
|
||||||
<Link to={`/files/${props.review.file.id}`}>
|
musician plays the guitar. They are all very good. They really make the
|
||||||
{props.review.file.filename}
|
song sound better. I like the way the bass plays and the way the guitar
|
||||||
</Link>{" "}
|
sounds. I like the way the drums sound.
|
||||||
{Tr("on")} {convertIntToDateTime(props.review.created_at)}{" "}
|
</p>
|
||||||
{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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
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,50 +1,20 @@
|
|||||||
import { useState, useEffect, useContext } from "react";
|
import { useState, useEffect } 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";
|
import searchFilesRespondExample from "../example-respond/search_files.json"
|
||||||
|
|
||||||
function SearchFiles(props) {
|
function SearchFiles(props) {
|
||||||
const navigator = useNavigate();
|
|
||||||
const [files, setFiles] = useState([]);
|
const [files, setFiles] = useState([]);
|
||||||
const query = useQuery();
|
const [filename, setFilename] = useState("");
|
||||||
const filename = query.get("q") || "";
|
const [offset, setOffset] = useState(0);
|
||||||
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
|
setFiles(searchFilesRespondExample.files);
|
||||||
if (filename === "") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setIsLoading(true);
|
|
||||||
fetch("/api/v1/search_files", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
filename: filename,
|
|
||||||
limit: limit,
|
|
||||||
offset: offset,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((data) => {
|
|
||||||
const files = data.files ? data.files : [];
|
|
||||||
setFiles(files);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
alert("search_files error: " + error);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextPage() {
|
function nextPage() {
|
||||||
navigator(`/files?q=${filenameInput}&o=${offset + limit}`);
|
setOffset(offset + limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
function lastPage() {
|
function lastPage() {
|
||||||
@@ -52,38 +22,43 @@ function SearchFiles(props) {
|
|||||||
if (offsetValue < 0) {
|
if (offsetValue < 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
navigator(`/files?q=${filenameInput}&o=${offsetValue}`);
|
setOffset(offsetValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => searchFiles(), [offset, filename]); // eslint-disable-line react-hooks/exhaustive-deps
|
useEffect(() => searchFiles(), [offset, props.folder]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<h3>{Tr("Search Files")}</h3>
|
<h3>Search Files</h3>
|
||||||
<div className="search_toolbar">
|
<div className="search_toolbar">
|
||||||
<input
|
{!props.folder && (
|
||||||
onChange={(event) => setFilenameInput(event.target.value)}
|
<input
|
||||||
onKeyDown={(event) => {
|
onChange={(event) => setFilename(event.target.value)}
|
||||||
if (event.key === "Enter") {
|
onKeyDown={(event) => {
|
||||||
navigator(`/files?q=${filenameInput}&o=0`);
|
if (event.key === "Enter") {
|
||||||
}
|
searchFiles();
|
||||||
}}
|
}
|
||||||
type="text"
|
}}
|
||||||
placeholder={tr("Enter filename", langCode)}
|
type="text"
|
||||||
value={filenameInput}
|
placeholder="Enter filename"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
|
disabled={!!props.folder}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator(`/files?q=${filenameInput}&o=0`);
|
searchFiles();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isLoading ? Tr("Loading...") : Tr("Search")}
|
{isLoading ? "Loading..." : "Search"}
|
||||||
</button>
|
</button>
|
||||||
<button onClick={lastPage}>{Tr("Last page")}</button>
|
{props.folder && props.folder.foldername && (
|
||||||
|
<button onClick={searchFiles}>{props.folder.foldername}</button>
|
||||||
|
)}
|
||||||
|
<button onClick={lastPage}>Last page</button>
|
||||||
<button disabled>
|
<button disabled>
|
||||||
{offset} - {offset + files.length}
|
{offset} - {offset + files.length}
|
||||||
</button>
|
</button>
|
||||||
<button onClick={nextPage}>{Tr("Next page")}</button>
|
<button onClick={nextPage}>Next page</button>
|
||||||
</div>
|
</div>
|
||||||
<FilesTable setPlayingFile={props.setPlayingFile} files={files} />
|
<FilesTable setPlayingFile={props.setPlayingFile} files={files} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,48 +1,23 @@
|
|||||||
import { useContext, useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useParams } from "react-router";
|
||||||
import { useQuery } from "./Common";
|
|
||||||
import FoldersTable from "./FoldersTable";
|
import FoldersTable from "./FoldersTable";
|
||||||
import { Tr, tr, langCodeContext } from "../translate";
|
import SearchFiles from "./SearchFiles";
|
||||||
|
import searchFoldersRespondExample from "../example-respond/search_folders.json";
|
||||||
|
|
||||||
function SearchFolders() {
|
function SearchFolders(props) {
|
||||||
const navigator = useNavigate();
|
const [foldername, setFoldername] = useState("");
|
||||||
const query = useQuery();
|
|
||||||
const foldername = query.get("q") || "";
|
|
||||||
const [foldernameInput, setFoldernameInput] = useState(foldername);
|
|
||||||
const [folders, setFolders] = useState([]);
|
const [folders, setFolders] = useState([]);
|
||||||
const offset = parseInt(query.get("o")) || 0;
|
const [folder, setFolder] = useState({});
|
||||||
|
const [offset, setOffset] = useState(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 === "") {
|
setFolders(searchFoldersRespondExample.folders);
|
||||||
return;
|
|
||||||
}
|
|
||||||
setIsLoading(true);
|
|
||||||
fetch("/api/v1/search_folders", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
foldername: foldername,
|
|
||||||
limit: limit,
|
|
||||||
offset: offset,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((data) => {
|
|
||||||
setFolders(data.folders ? data.folders : []);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
alert("search_folders error: " + error);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextPage() {
|
function nextPage() {
|
||||||
navigator(`/folders?q=${foldername}&o=${offset + limit}`);
|
setOffset(offset + limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
function lastPage() {
|
function lastPage() {
|
||||||
@@ -50,40 +25,46 @@ function SearchFolders() {
|
|||||||
if (offsetValue < 0) {
|
if (offsetValue < 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
navigator(`/folders?q=${foldername}&o=${offsetValue}`);
|
setOffset(offsetValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => searchFolder(), [offset, foldername]); // eslint-disable-line react-hooks/exhaustive-deps
|
function viewFolder(folder) {
|
||||||
|
setFolder(folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
let params = useParams();
|
||||||
|
useEffect(() => searchFolder(), [offset]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
useEffect(() => {
|
||||||
|
if (params.id !== undefined) {
|
||||||
|
setFolder({ id: parseInt(params.id) });
|
||||||
|
}
|
||||||
|
}, [params.id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<h3>{Tr("Search Folders")}</h3>
|
<h3>Search Folders</h3>
|
||||||
<div className="search_toolbar">
|
<div className="search_toolbar">
|
||||||
<input
|
<input
|
||||||
onChange={(event) => setFoldernameInput(event.target.value)}
|
onChange={(event) => setFoldername(event.target.value)}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === "Enter") {
|
if (event.key === "Enter") {
|
||||||
navigator(`/folders?q=${foldernameInput}&o=0`);
|
searchFolder();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={tr("Enter folder name", langCode)}
|
placeholder="Enter folder name"
|
||||||
value={foldernameInput}
|
|
||||||
/>
|
/>
|
||||||
<button
|
<button onClick={searchFolder}>
|
||||||
onClick={() => {
|
{isLoading ? "Loading..." : "Search"}
|
||||||
navigator(`/folders?q=${foldernameInput}&o=0`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isLoading ? Tr("Loading...") : Tr("Search")}
|
|
||||||
</button>
|
</button>
|
||||||
<button onClick={lastPage}>{Tr("Last page")}</button>
|
<button onClick={lastPage}>Last page</button>
|
||||||
<button disabled>
|
<button disabled>
|
||||||
{offset} - {offset + limit}
|
{offset} - {offset + limit}
|
||||||
</button>
|
</button>
|
||||||
<button onClick={nextPage}>{Tr("Next page")}</button>
|
<button onClick={nextPage}>Next page</button>
|
||||||
</div>
|
</div>
|
||||||
<FoldersTable folders={folders} />
|
<FoldersTable viewFolder={viewFolder} folders={folders} />
|
||||||
|
<SearchFiles setPlayingFile={props.setPlayingFile} folder={folder} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +1,25 @@
|
|||||||
import { useContext, useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useParams } from "react-router";
|
import { useParams } from "react-router";
|
||||||
import FilesTable from "./FilesTable";
|
import FilesTable from "./FilesTable";
|
||||||
import { Tr, tr, langCodeContext } from "../translate";
|
import GetFileInfoRespondExample from "../example-respond/get_file_info.json";
|
||||||
|
|
||||||
function Share(props) {
|
function Share(props) {
|
||||||
let params = useParams();
|
let params = useParams();
|
||||||
const { langCode } = useContext(langCodeContext);
|
const [file, setFile] = useState([]);
|
||||||
|
|
||||||
const [file, setFile] = useState({});
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch("/api/v1/get_file_info", {
|
setFile([GetFileInfoRespondExample]);
|
||||||
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]);
|
}, [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 (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<h3>{Tr("Share with others!")}</h3>
|
<h3>Share with others!</h3>
|
||||||
<p>
|
<p>
|
||||||
{Tr("Share link")}:{" "}
|
👇 Click the filename below to enjoy music!
|
||||||
<a href={window.location.href}>{window.location.href}</a>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
👇 {Tr("Click the filename below to enjoy music!")}
|
|
||||||
<br />
|
<br />
|
||||||
</p>
|
</p>
|
||||||
<FilesTable setPlayingFile={props.setPlayingFile} files={[file]} />
|
<p>
|
||||||
|
Share link: <a href={window.location.href}>{window.location.href}</a>
|
||||||
|
</p>
|
||||||
|
<FilesTable setPlayingFile={props.setPlayingFile} files={file} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
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;
|
|
||||||
23
web/src/component/User.js
Normal file
23
web/src/component/User.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { useNavigate } from "react-router";
|
||||||
|
|
||||||
|
function User(props) {
|
||||||
|
// props.user
|
||||||
|
// props.setUser
|
||||||
|
let navigate = useNavigate();
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="avatar"
|
||||||
|
onClick={() => {
|
||||||
|
if (props.user) {
|
||||||
|
navigate("/profile/" + props.user.id);
|
||||||
|
} else {
|
||||||
|
navigate("/login");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.user ? props.user.username : "Login"}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default User;
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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;
|
|
||||||
12
web/src/example-respond/get_ffmpeg_config_list.json
Normal file
12
web/src/example-respond/get_ffmpeg_config_list.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"ffmpeg_config_list": [
|
||||||
|
{ "name": "OPUS 128k", "args": "-c:a libopus -ab 128k" },
|
||||||
|
{ "name": "OPUS 96k", "args": "-c:a libopus -ab 96k" },
|
||||||
|
{ "name": "OPUS 256k", "args": "-c:a libopus -ab 256k" },
|
||||||
|
{ "name": "OPUS 320k", "args": "-c:a libopus -ab 320k" },
|
||||||
|
{ "name": "OPUS 512k", "args": "-c:a libopus -ab 512k" },
|
||||||
|
{ "name": "AAC 128k", "args": "-c:a aac -ab 128k" },
|
||||||
|
{ "name": "AAC 256k", "args": "-c:a aac -ab 256k" },
|
||||||
|
{ "name": "全损音质 32k", "args": "-c:a libopus -ab 32k" }
|
||||||
|
]
|
||||||
|
}
|
||||||
7
web/src/example-respond/get_file_info.json
Normal file
7
web/src/example-respond/get_file_info.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"id": 9856,
|
||||||
|
"folder_id": 898,
|
||||||
|
"foldername": "[2021.05.12] TVアニメ「シャドーハウス」EDテーマ「ないない」/ReoNa [スペシャルエディション] [FLAC 96kHz/24bit]",
|
||||||
|
"filename": "03. 生きてるだけでえらいよ.flac",
|
||||||
|
"filesize": 122761032
|
||||||
|
}
|
||||||
74
web/src/example-respond/get_random_files.json
Normal file
74
web/src/example-respond/get_random_files.json
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"id": 9727,
|
||||||
|
"folder_id": 958,
|
||||||
|
"foldername": "garnet (narry) — emerald [GARN-0002] (flac)",
|
||||||
|
"filename": "06. narry — malachite.flac",
|
||||||
|
"filesize": 28228112
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4785,
|
||||||
|
"folder_id": 457,
|
||||||
|
"foldername": "Winter (FLAC)",
|
||||||
|
"filename": "08 - 恋.flac",
|
||||||
|
"filesize": 33576086
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13943,
|
||||||
|
"folder_id": 1368,
|
||||||
|
"foldername": "[mikudb] 融合YELLOWS",
|
||||||
|
"filename": "10. カレーライスのうた.mp3",
|
||||||
|
"filesize": 2524925
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 21743,
|
||||||
|
"folder_id": 2207,
|
||||||
|
"foldername": "Tsumanne\(^o^)/",
|
||||||
|
"filename": "08.スーパートルコ行進曲 - オワタ\(^o^)/.mp3",
|
||||||
|
"filesize": 7677985
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 35918,
|
||||||
|
"folder_id": 3758,
|
||||||
|
"foldername": "2008 - Higurashi 2 - Ano hi, Ano Basho, Subete ni ' Arigatou",
|
||||||
|
"filename": "06 - Thanks (Bashee Arenge Version).flac",
|
||||||
|
"filesize": 39545977
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 32394,
|
||||||
|
"folder_id": 3341,
|
||||||
|
"foldername": "[彩音 ~xi-on~] Eyes",
|
||||||
|
"filename": "08 - 少女さとり ~ 3rd Eye.mp3",
|
||||||
|
"filesize": 7538525
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 16934,
|
||||||
|
"folder_id": 1671,
|
||||||
|
"foldername": "Explorism",
|
||||||
|
"filename": "14.Please, My Producer (feat. SAK).mp3",
|
||||||
|
"filesize": 6099717
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1381,
|
||||||
|
"folder_id": 131,
|
||||||
|
"foldername": "Garakuta Live (FLAC)",
|
||||||
|
"filename": "(09) [しけもく] 愛染エピローグ.flac",
|
||||||
|
"filesize": 25173829
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 18066,
|
||||||
|
"folder_id": 1791,
|
||||||
|
"foldername": "Jailbreak from the Sunday Morning!!",
|
||||||
|
"filename": "05. Good bye!! Melancory Sunday Morning!!.mp3",
|
||||||
|
"filesize": 10197778
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 41261,
|
||||||
|
"folder_id": 4305,
|
||||||
|
"foldername": "[KSLA-0124~6] Modification of Key Sounds Label [CD-FLAC]",
|
||||||
|
"filename": "3-01. Trigger (MUZIK SERVANT vs Freezer Remix).flac",
|
||||||
|
"filesize": 42772516
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
25
web/src/example-respond/search_files.json
Normal file
25
web/src/example-respond/search_files.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"id": 26555,
|
||||||
|
"folder_id": 2579,
|
||||||
|
"foldername": "[mikudb] SEB presents SUPER HATSUNE BEAT Vol. 1",
|
||||||
|
"filename": "02. White Letter (ゆうゆP Euro Arrange).mp3",
|
||||||
|
"filesize": 4252006
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 40891,
|
||||||
|
"folder_id": 4121,
|
||||||
|
"foldername": "初音ミクベスト ~memories~",
|
||||||
|
"filename": "10.White letter.mp3",
|
||||||
|
"filesize": 6723982
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 43289,
|
||||||
|
"folder_id": 4384,
|
||||||
|
"foldername": "Hatsune Miku Best ~memories~",
|
||||||
|
"filename": "10.White Letter.flac",
|
||||||
|
"filesize": 20817678
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
32
web/src/example-respond/search_folders.json
Normal file
32
web/src/example-respond/search_folders.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"id": 2037,
|
||||||
|
"foldername": "P∴Rhythmatiq — 七色リミックス [OSLA-0005] (flac+scans)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2130,
|
||||||
|
"foldername": " P∴Rhythmatiq — P∴Rhythmatiq act:09 [PRTQ-0017] (flac+scans)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2176,
|
||||||
|
"foldername": "P∴Rhythmatiq — P∴Rhythmatiq act:02 [PRTQ-0002] (flac)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2184,
|
||||||
|
"foldername": "P∴Rhythmatiq — P∴Rhythmatiq act:04 [PRTQ-0005] (flac)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2190,
|
||||||
|
"foldername": "P∴Rhythmatiq — P∴Rhythmatiq Rock!! [PQPC-0001] (flac)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2360,
|
||||||
|
"foldername": "P∴Rhythmatiq — P∴Rhythmatiq act:11 [PRTQ-0025] (flac)"
|
||||||
|
},
|
||||||
|
{ "id": 3443, "foldername": "P∴Rhythmatiq EXTRA" },
|
||||||
|
{ "id": 3444, "foldername": "P∴Rhythmatiq He:arts" },
|
||||||
|
{ "id": 3445, "foldername": "P∴Rhythmatiq Re act" },
|
||||||
|
{ "id": 3446, "foldername": "P∴Rhythmatiq Rock!!" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import { createContext, renderToString } from "react";
|
|
||||||
import MAP_zh_CN from "./zh_CN";
|
|
||||||
|
|
||||||
const LANG_OPTIONS = {
|
|
||||||
"en-US": {
|
|
||||||
name: "English",
|
|
||||||
langMap: {},
|
|
||||||
matches: ["en-US", "en"],
|
|
||||||
},
|
|
||||||
"zh-CN": {
|
|
||||||
name: "中文(简体)",
|
|
||||||
langMap: MAP_zh_CN,
|
|
||||||
matches: ["zh-CN", "zh"],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const langCodeContext = createContext("en-US");
|
|
||||||
|
|
||||||
function tr(text, langCode) {
|
|
||||||
const option = LANG_OPTIONS[langCode];
|
|
||||||
if (option === undefined) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
const langMap = LANG_OPTIONS[langCode].langMap;
|
|
||||||
|
|
||||||
const translatedText = langMap[text.toLowerCase()];
|
|
||||||
if (translatedText === undefined) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
return translatedText;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Tr(text) {
|
|
||||||
return (
|
|
||||||
<langCodeContext.Consumer>
|
|
||||||
{({ langCode }) => {
|
|
||||||
return tr(text, langCode);
|
|
||||||
}}
|
|
||||||
</langCodeContext.Consumer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { tr, Tr, LANG_OPTIONS, langCodeContext };
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
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