Compare commits
2 Commits
test-1
...
submit-web
| Author | SHA1 | Date | |
|---|---|---|---|
|
b6013ba1e3
|
|||
|
3457fde522
|
18
.drone.yml
18
.drone.yml
@@ -1,18 +0,0 @@
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: default
|
||||
|
||||
steps:
|
||||
- name: frontend-web
|
||||
image: node:19
|
||||
commands:
|
||||
- cd web
|
||||
- npm install
|
||||
- npm run build
|
||||
|
||||
- name: release
|
||||
image: plugins/gitea-release
|
||||
settings:
|
||||
api_key: da966507c259aa32ccc2d434e930af4a580de785
|
||||
base_url: https://yongyuancv.cn/git/
|
||||
files: build/*
|
||||
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
|
||||
web:
|
||||
dist:
|
||||
cd web && npm install
|
||||
cd web && npm run build
|
||||
|
||||
linux:
|
||||
go build -v -ldflags '-linkmode=external -extldflags=-static' -tags sqlite_omit_load_extension,netgo
|
||||
go build
|
||||
|
||||
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
|
||||
|
||||
[](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
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
- `addr` string type. The listen address and port.
|
||||
- `token` string type. Password.
|
||||
- `ffmpeg_config_list` list type, include `ffmpegConfig` object.
|
||||
- `file_life_time` integer type (second). Life time for temporary file. If the temporary file is not accessed for more than this time, back-end server will delete this file.
|
||||
- `cleaner_internal` integer type (second). Interval for `tmpfs` checking temporary file.
|
||||
- `root` string type. Directory to store temporary files. Default is `/tmp`, **please modify this directory if you are using Windows.** Directory will be created if not exists.
|
||||
- `permission`. Specify each API's permission level.
|
||||
- `0` for no permission required.
|
||||
- `1` require admin level (highest level) permission.
|
||||
- `2` require normal user level permission. That is, both admins and registered users can access to this URL, except anonymous users.
|
||||
- If you want to avoid abuse of the playback API, you can adjust the permission level for these 5 playback-related APIs.
|
||||
- `/get_file` get file with `io.copy()` method
|
||||
- `/get_file_direct` get file with `http.serveFile()` method
|
||||
- `/get_file_stream` call ffmpeg and stream its `stdout` output
|
||||
- `/prepare_file_stream_direct` call ffmpeg to convert a file
|
||||
- `/get_file_stream_direct` get the converted file with `http.serveFile()`
|
||||
- Other URLs not metion in `config.json` will have `0` permission level by default.
|
||||
- `root` string type. Directory to store temporary files. Default is `/tmp`, **please modify this directory if you are using Windows.**
|
||||
|
||||
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
|
||||
|
||||
- `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.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
```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
|
||||
{
|
||||
"error": "Wrong password"
|
||||
}
|
||||
```
|
||||
Anonymous API can be called by anonymous.
|
||||
|
||||
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": {
|
||||
"secret": "CHANGE_YOUR_SECRET_HERE",
|
||||
"database_name": "postgres://postgres:woshimima@localhost/postgres?sslmode=disable",
|
||||
"single_thread": true,
|
||||
"addr": ":8080",
|
||||
"ffmpeg_threads": 1,
|
||||
"ffmpeg_config_list": [
|
||||
{
|
||||
"name": "WEBM OPUS 128k",
|
||||
"args": "-c:a libopus -ab 128k -vn",
|
||||
"format": "webm"
|
||||
},
|
||||
{
|
||||
"name": "WEBM OPUS 96k",
|
||||
"args": "-c:a libopus -ab 96k -vn",
|
||||
"format": "webm"
|
||||
},
|
||||
{
|
||||
"name": "WEBM OPUS 256k",
|
||||
"args": "-c:a libopus -ab 256k -vn",
|
||||
"format": "webm"
|
||||
},
|
||||
{
|
||||
"name": "WEBM OPUS 512k",
|
||||
"args": "-c:a libopus -ab 512k -vn",
|
||||
"format": "webm"
|
||||
},
|
||||
{
|
||||
"name": "AAC 128k",
|
||||
"args": "-c:a aac -ab 128k -vn",
|
||||
"format": "adts"
|
||||
},
|
||||
{
|
||||
"name": "AAC 256k",
|
||||
"args": "-c:a aac -ab 256k -vn",
|
||||
"format": "adts"
|
||||
},
|
||||
{ "name": "MP3 128k", "args": "-c:a mp3 -ab 128k -vn", "format": "mp3" },
|
||||
{ "name": "MP3 320k", "args": "-c:a mp3 -ab 320k -vn", "format": "mp3" },
|
||||
{
|
||||
"name": "全损音质 8k",
|
||||
"args": "-c:a libopus -ab 8k -vn",
|
||||
"format": "webm"
|
||||
}
|
||||
],
|
||||
"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/"
|
||||
}
|
||||
"api": {
|
||||
"database_name": "music.sqlite3",
|
||||
"addr": ":8080",
|
||||
"token": "!! config your very strong token here !!",
|
||||
"ffmpeg_threads": 1,
|
||||
"ffmpeg_config_list": [
|
||||
{"name": "OPUS 128k", "args": "-c:a libopus -ab 128k"},
|
||||
{"name": "OPUS 96k", "args": "-c:a libopus -ab 96k"},
|
||||
{"name": "OPUS 256k", "args": "-c:a libopus -ab 256k"},
|
||||
{"name": "OPUS 320k", "args": "-c:a libopus -ab 320k"},
|
||||
{"name": "OPUS 512k", "args": "-c:a libopus -ab 512k"},
|
||||
{"name": "AAC 128k", "args": "-c:a aac -ab 128k"},
|
||||
{"name": "AAC 256k", "args": "-c:a aac -ab 256k"},
|
||||
{"name": "全损音质 32k", "args": "-c:a libopus -ab 32k"}
|
||||
]
|
||||
},
|
||||
"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
|
||||
|
||||
go 1.18
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/lib/pq v1.10.7
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
|
||||
)
|
||||
|
||||
require github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
require github.com/mattn/go-sqlite3 v1.14.7 // 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/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
|
||||
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
github.com/mattn/go-sqlite3 v1.14.7 h1:fxWBnXkxfM6sRiuH3bqJ4CfzZojMOLVc0UTsTglEghA=
|
||||
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
|
||||
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"
|
||||
"flag"
|
||||
"log"
|
||||
"msw-open-music/pkg/api"
|
||||
"msw-open-music/pkg/commonconfig"
|
||||
"msw-open-music/internal/pkg/api"
|
||||
"os"
|
||||
)
|
||||
|
||||
@@ -15,11 +14,12 @@ func init() {
|
||||
flag.StringVar(&ConfigFilePath, "config", "config.json", "backend config file path")
|
||||
}
|
||||
|
||||
|
||||
func main() {
|
||||
var err error
|
||||
flag.Parse()
|
||||
|
||||
config := commonconfig.Config{}
|
||||
config := api.Config{}
|
||||
configFile, err := os.Open(ConfigFilePath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -37,6 +37,7 @@ func main() {
|
||||
log.Println("Starting",
|
||||
config.APIConfig.DatabaseName,
|
||||
config.APIConfig.Addr,
|
||||
config.APIConfig.Token,
|
||||
)
|
||||
log.Fatal(api.Server.ListenAndServe())
|
||||
}
|
||||
|
||||
109
pkg/api/api.go
109
pkg/api/api.go
@@ -1,109 +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_ffprobe_info", api.HandleGetFileFfprobeInfo)
|
||||
apiMux.HandleFunc("/get_file_stream_direct", api.HandleGetFileStreamDirect)
|
||||
apiMux.HandleFunc("/get_file_avatar", api.HandelGetFileAvatar)
|
||||
apiMux.HandleFunc("/prepare_file_stream_direct", api.HandlePrepareFileStreamDirect)
|
||||
apiMux.HandleFunc("/delete_file", api.HandleDeleteFile)
|
||||
apiMux.HandleFunc("/update_filename", api.HandleUpdateFilename)
|
||||
apiMux.HandleFunc("/reset_filename", api.HandleResetFilename)
|
||||
apiMux.HandleFunc("/reset_foldername", api.HandleResetFoldername)
|
||||
// feedback
|
||||
apiMux.HandleFunc("/feedback", api.HandleFeedback)
|
||||
apiMux.HandleFunc("/get_feedbacks", api.HandleGetFeedbacks)
|
||||
apiMux.HandleFunc("/delete_feedback", api.HandleDeleteFeedback)
|
||||
// user
|
||||
apiMux.HandleFunc("/login", api.HandleLogin)
|
||||
apiMux.HandleFunc("/register", api.HandleRegister)
|
||||
apiMux.HandleFunc("/logout", api.HandleLoginAsAnonymous)
|
||||
apiMux.HandleFunc("/get_user_info", api.HandleGetUserInfo)
|
||||
apiMux.HandleFunc("/get_users", api.HandleGetUsers)
|
||||
apiMux.HandleFunc("/update_user_active", api.HandleUpdateUserActive)
|
||||
apiMux.HandleFunc("/update_username", api.HandleUpdateUsername)
|
||||
apiMux.HandleFunc("/update_user_password", api.HandleUpdateUserPassword)
|
||||
// tag
|
||||
apiMux.HandleFunc("/get_tags", api.HandleGetTags)
|
||||
apiMux.HandleFunc("/get_tag_info", api.HandleGetTagInfo)
|
||||
apiMux.HandleFunc("/insert_tag", api.HandleInsertTag)
|
||||
apiMux.HandleFunc("/update_tag", api.HandleUpdateTag)
|
||||
apiMux.HandleFunc("/put_tag_on_file", api.HandlePutTagOnFile)
|
||||
apiMux.HandleFunc("/get_tags_on_file", api.HandleGetTagsOnFile)
|
||||
apiMux.HandleFunc("/delete_tag_on_file", api.HandleDeleteTagOnFile)
|
||||
apiMux.HandleFunc("/delete_tag", api.HandleDeleteTag)
|
||||
// folder
|
||||
apiMux.HandleFunc("/update_foldername", api.HandleUpdateFoldername)
|
||||
// review
|
||||
apiMux.HandleFunc("/insert_review", api.HandleInsertReview)
|
||||
apiMux.HandleFunc("/get_reviews_on_file", api.HandleGetReviewsOnFile)
|
||||
apiMux.HandleFunc("/get_review", api.HandleGetReview)
|
||||
apiMux.HandleFunc("/update_review", api.HandleUpdateReview)
|
||||
apiMux.HandleFunc("/delete_review", api.HandleDeleteReview)
|
||||
apiMux.HandleFunc("/get_reviews_by_user", api.HandleGetReviewsByUser)
|
||||
// statistic
|
||||
apiMux.HandleFunc("/record_playback", api.HandleRecordPlayback)
|
||||
// database
|
||||
apiMux.HandleFunc("/walk", api.HandleWalk)
|
||||
apiMux.HandleFunc("/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,108 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"msw-open-music/pkg/database"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (api *API) HandelGetFileAvatar(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
q := r.URL.Query()
|
||||
ids := q["id"]
|
||||
if len(ids) == 0 {
|
||||
err = errors.New(`parameter "id" can't be empty`)
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
id, err := strconv.Atoi(ids[0])
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
file, err := api.Db.GetFile(int64(id))
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
path, err := file.Path()
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
log.Println("[api] Get avatar of file", path)
|
||||
buff := make([]byte, 0)
|
||||
cache := bytes.NewBuffer(buff)
|
||||
cmd := exec.Command("ffmpeg", "-i", path, "-c:v", "libwebp_anim", "-update", "1", "-frames:v", "1", "-f", "image2pipe", "-")
|
||||
cmd.Stdout = cache
|
||||
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
api.HandleGetAlternativeFileAvatar(w, r, file)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "image/webp")
|
||||
io.Copy(w, cache)
|
||||
}
|
||||
|
||||
func (api *API) HandleGetAlternativeFileAvatar(w http.ResponseWriter, r *http.Request, f *database.File) {
|
||||
var err error
|
||||
dir, err := f.Dir()
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
log.Println("[api] Get alternative avatar in dir", dir)
|
||||
files, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
avatar, err := findAvatarFile(files)
|
||||
avatarPath := path.Join(dir, avatar)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
cmd := exec.Command("ffmpeg", "-i", avatarPath, "-c:v", "libwebp_anim", "-f", "image2pipe", "-")
|
||||
cmd.Stdout = w
|
||||
w.Header().Set("Content-Type", "image/webp")
|
||||
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func findAvatarFile(files []os.DirEntry) (string, error) {
|
||||
for _, file := range files {
|
||||
if isAvatarType(file.Name()) {
|
||||
return file.Name(), nil
|
||||
}
|
||||
}
|
||||
return "", errors.New("Cannot find avatar file")
|
||||
}
|
||||
|
||||
var avatarFileTypes = []string{
|
||||
".jpg",
|
||||
".png",
|
||||
}
|
||||
|
||||
func isAvatarType(filename string) bool {
|
||||
for _, t := range avatarFileTypes {
|
||||
if strings.HasSuffix(strings.ToLower(filename), t) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -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,165 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"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
|
||||
}
|
||||
}
|
||||
|
||||
func (api *API) HandleGetFileFfprobeInfo(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 Ffprobe info", getFileRequest.ID)
|
||||
|
||||
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
|
||||
}
|
||||
cmd := exec.Command("ffprobe", "-i", path, "-hide_banner")
|
||||
cmd.Stderr = w
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// /api/v1/get_file?id=123
|
||||
// get raw file with io.Copy method
|
||||
func (api *API) HandleGetFile(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
ids := q["id"]
|
||||
_id, err := strconv.Atoi(ids[0])
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
id := int64(_id)
|
||||
|
||||
// check empty
|
||||
if id < 0 {
|
||||
api.HandleErrorString(w, r, `"id" can't be none or negative`)
|
||||
return
|
||||
}
|
||||
|
||||
file, err := api.Db.GetFile(id)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
path, err := file.Path()
|
||||
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,52 +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"`
|
||||
Folder string `json:"folder"`
|
||||
}
|
||||
|
||||
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, folder, err := api.Db.GetFilesInFolder(getFilesInFolderRequest.Folder_id, getFilesInFolderRequest.Limit, getFilesInFolderRequest.Offset)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
getFilesInFolderResponse := &GetFilesInFolderResponse{
|
||||
Files: &files,
|
||||
Folder: folder,
|
||||
}
|
||||
|
||||
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,51 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"msw-open-music/pkg/database"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RecordPlaybackRequest struct {
|
||||
Playback database.Playback `json:"playback"`
|
||||
}
|
||||
|
||||
func (api *API) HandleRecordPlayback(w http.ResponseWriter, r *http.Request) {
|
||||
recordPlaybackRequest := &RecordPlaybackRequest{}
|
||||
err := json.NewDecoder(r.Body).Decode(recordPlaybackRequest)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
recordPlaybackRequest.Playback.Time = time.Now()
|
||||
recordPlaybackRequest.Playback.UserID, err = api.GetUserID(w, r)
|
||||
if err != nil {
|
||||
if err == ErrNotLoggedIn {
|
||||
user, err := api.Db.LoginAsAnonymous()
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
recordPlaybackRequest.Playback.UserID = user.ID
|
||||
} else {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("[api] Record playback history",
|
||||
recordPlaybackRequest.Playback.UserID,
|
||||
recordPlaybackRequest.Playback.FileID,
|
||||
recordPlaybackRequest.Playback.Duration,
|
||||
recordPlaybackRequest.Playback.Method)
|
||||
|
||||
err = api.Db.RecordPlayback(recordPlaybackRequest.Playback)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
api.HandleOK(w, r)
|
||||
}
|
||||
@@ -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,326 +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) HandleLoginAsAnonymous(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := api.LoginAsAnonymous(w, r)
|
||||
resp := &LoginResponse{
|
||||
User: user,
|
||||
}
|
||||
|
||||
err = json.NewEncoder(w).Encode(resp)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (api *API) LoginAsAnonymous(w http.ResponseWriter, r *http.Request) (*database.User, error) {
|
||||
user, err := api.Db.LoginAsAnonymous()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
session, _ := api.store.Get(r, api.defaultSessionName)
|
||||
|
||||
// save session
|
||||
session.Values["userId"] = user.ID
|
||||
err = session.Save(r, w)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// return user
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (api *API) HandleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
var user *database.User
|
||||
var err error
|
||||
session, _ := api.store.Get(r, api.defaultSessionName)
|
||||
|
||||
// Get method will login current or anonymous user
|
||||
if r.Method == "GET" {
|
||||
|
||||
// if user already logged in
|
||||
if userId, ok := session.Values["userId"]; ok {
|
||||
user, err = api.Db.GetUserById(userId.(int64))
|
||||
if err != nil {
|
||||
if err != sql.ErrNoRows {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
log.Println("[api] Warning: User not found")
|
||||
// login as anonymous user
|
||||
api.LoginAsAnonymous(w, r)
|
||||
return
|
||||
}
|
||||
log.Println("[api] User already logged in:", user)
|
||||
|
||||
} else {
|
||||
// login as anonymous user
|
||||
log.Println("[api] Login as anonymous user")
|
||||
api.LoginAsAnonymous(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
var request LoginRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&request)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("Login as user", request.Username)
|
||||
|
||||
user, err = api.Db.Login(request.Username, request.Password)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// if user is not active
|
||||
if !user.Active {
|
||||
api.HandleError(w, r, ErrNotActive)
|
||||
return
|
||||
}
|
||||
|
||||
// save session
|
||||
session.Values["userId"] = user.ID
|
||||
err = session.Save(r, w)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
resp := &LoginResponse{
|
||||
User: user,
|
||||
}
|
||||
err = json.NewEncoder(w).Encode(resp)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type RegisterRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Role int64 `json:"role"`
|
||||
}
|
||||
|
||||
func (api *API) HandleRegister(w http.ResponseWriter, r *http.Request) {
|
||||
var request RegisterRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&request)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("[api] Register user", request.Username)
|
||||
|
||||
err = api.Db.Register(request.Username, request.Password, request.Role)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.HandleOK(w, r)
|
||||
}
|
||||
|
||||
func (api *API) GetUserID(w http.ResponseWriter, r *http.Request) (int64, error) {
|
||||
session, _ := api.store.Get(r, api.defaultSessionName)
|
||||
userId, ok := session.Values["userId"]
|
||||
if !ok {
|
||||
return 0, ErrNotLoggedIn
|
||||
}
|
||||
|
||||
return userId.(int64), nil
|
||||
}
|
||||
|
||||
type GetUsersResponse struct {
|
||||
Users []*database.User `json:"users"`
|
||||
}
|
||||
|
||||
func (api *API) HandleGetUsers(w http.ResponseWriter, r *http.Request) {
|
||||
users, err := api.Db.GetUsers()
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
ret := &GetUsersResponse{
|
||||
Users: users,
|
||||
}
|
||||
|
||||
err = json.NewEncoder(w).Encode(ret)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type UpdateUserActiveRequest struct {
|
||||
ID int64 `json:"id"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
func (api *API) HandleUpdateUserActive(w http.ResponseWriter, r *http.Request) {
|
||||
req := &UpdateUserActiveRequest{}
|
||||
err := json.NewDecoder(r.Body).Decode(req)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = api.Db.UpdateUserActive(req.ID, req.Active)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
api.HandleOK(w, r)
|
||||
}
|
||||
|
||||
type UpdateUsernameRequest struct {
|
||||
ID int64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
func (api *API) HandleUpdateUsername(w http.ResponseWriter, r *http.Request) {
|
||||
// middileware reject anonymous user
|
||||
|
||||
req := &UpdateUsernameRequest{}
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(req)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := api.Db.GetUserById(req.ID)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := api.GetUserID(w, r)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if user.ID != userID && user.Role != database.RoleAdmin {
|
||||
api.HandleError(w, r, ErrNotAdmin)
|
||||
return
|
||||
}
|
||||
|
||||
err = api.Db.UpdateUsername(req.ID, req.Username)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.HandleOK(w, r)
|
||||
}
|
||||
|
||||
type GetUserInfoRequest struct {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
type GetUserInfoResponse struct {
|
||||
User *database.User `json:"user"`
|
||||
}
|
||||
|
||||
func (api *API) HandleGetUserInfo(w http.ResponseWriter, r *http.Request) {
|
||||
req := &GetUserInfoRequest{}
|
||||
err := json.NewDecoder(r.Body).Decode(req)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := api.Db.GetUserById(req.ID)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
ret := &GetUserInfoResponse{
|
||||
User: user,
|
||||
}
|
||||
|
||||
err = json.NewEncoder(w).Encode(ret)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type UpdateUserPasswordRequest struct {
|
||||
ID int64 `json:"id"`
|
||||
OldPassword string `json:"old_password"`
|
||||
NewPassword string `json:"new_password"`
|
||||
}
|
||||
|
||||
func (api *API) HandleUpdateUserPassword(w http.ResponseWriter, r *http.Request) {
|
||||
// middleware reject anonymous user
|
||||
|
||||
req := &UpdateUserPasswordRequest{}
|
||||
err := json.NewDecoder(r.Body).Decode(req)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := api.Db.GetUserById(req.ID)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := api.GetUserID(w, r)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
currentUser, err := api.Db.GetUserById(userID)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
if currentUser.Role != database.RoleAdmin {
|
||||
_, err := api.Db.Login(user.Username, req.OldPassword)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, ErrWrongPassword)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = api.Db.UpdateUserPassword(req.ID, req.NewPassword)
|
||||
if err != nil {
|
||||
api.HandleError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
api.HandleOK(w, r)
|
||||
}
|
||||
@@ -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/lib/pq"
|
||||
)
|
||||
|
||||
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("postgres", 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,456 +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, string, error) {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
rows, err := database.stmt.getFilesInFolder.Query(folder_id, limit, offset)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer rows.Close()
|
||||
files := make([]File, 0)
|
||||
folder := ""
|
||||
for rows.Next() {
|
||||
file := File{
|
||||
Db: database,
|
||||
Folder_id: folder_id,
|
||||
}
|
||||
err = rows.Scan(&file.ID, &file.Filename, &file.Filesize, &file.Foldername, &folder)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
return files, folder, nil
|
||||
}
|
||||
|
||||
func (database *Database) SearchFolders(foldername string, limit int64, offset int64) ([]Folder, error) {
|
||||
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)
|
||||
}
|
||||
|
||||
tx, err := database.sqlConn.Begin()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
insertFolderStmt := tx.Stmt(database.stmt.insertFolder)
|
||||
insertFileStmt := tx.Stmt(database.stmt.insertFile)
|
||||
putTagOnFileStmt := tx.Stmt(database.stmt.putTagOnFile)
|
||||
findFolderStmt := tx.Stmt(database.stmt.findFolder)
|
||||
findFileStmt := tx.Stmt(database.stmt.findFile)
|
||||
|
||||
err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// check pattern
|
||||
ext := filepath.Ext(info.Name())
|
||||
if _, ok := patternDict[ext]; !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// insert file and folder
|
||||
// fileID, err := database.Insert(path, info.Size())
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
var folderID int64
|
||||
folder, filename := filepath.Split(path)
|
||||
err = findFolderStmt.QueryRow(folder).Scan(&folderID)
|
||||
if err != nil {
|
||||
result, err := insertFolderStmt.Query(folder, filepath.Base(folder))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for result.Next() {
|
||||
err = result.Scan(&folderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// try find file id
|
||||
var fileID int64
|
||||
result, err := findFileStmt.Query(folderID, filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for result.Next() {
|
||||
err = result.Scan(&fileID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// insert new file
|
||||
result, err = insertFileStmt.Query(folderID, filename, filename, info.Size())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for result.Next() {
|
||||
err = result.Scan(&fileID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, tag := range tags {
|
||||
_, err := putTagOnFileStmt.Exec(tag.ID, fileID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
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,7 +0,0 @@
|
||||
package database
|
||||
|
||||
func (database *Database) RecordPlayback(playback Playback) error {
|
||||
_, err := database.stmt.recordPlaybackStmt.Exec(
|
||||
playback.UserID, playback.FileID, playback.Time, playback.Method, playback.Duration)
|
||||
return err
|
||||
}
|
||||
@@ -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,116 +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.Query(tag.Name, tag.Description, tag.CreatedByUserId)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var id int64
|
||||
for result.Next() {
|
||||
err = result.Scan(&id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (database *Database) GetTag(id int64) (*Tag, error) {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
tag := &Tag{CreatedByUser: &User{}}
|
||||
err := database.stmt.getTag.QueryRow(id).Scan(
|
||||
&tag.ID, &tag.Name, &tag.Description,
|
||||
&tag.CreatedByUser.ID, &tag.CreatedByUser.Username, &tag.CreatedByUser.Role, &tag.CreatedByUser.AvatarId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tag, nil
|
||||
}
|
||||
|
||||
func (database *Database) GetTags() ([]*Tag, error) {
|
||||
tags := []*Tag{}
|
||||
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
rows, err := database.stmt.getTags.Query()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
tag := &Tag{CreatedByUser: &User{}}
|
||||
err := rows.Scan(
|
||||
&tag.ID, &tag.Name, &tag.Description,
|
||||
&tag.CreatedByUser.ID, &tag.CreatedByUser.Username, &tag.CreatedByUser.Role, &tag.CreatedByUser.AvatarId)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func (database *Database) UpdateTag(tag *Tag) error {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
result, err := database.stmt.updateTag.Exec(tag.Name, tag.Description, tag.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
return errors.New("No rows affected")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// delete tag and all its references in file_has_tag
|
||||
func (database *Database) DeleteTag(id int64) error {
|
||||
database.singleThreadLock.Lock()
|
||||
defer database.singleThreadLock.Unlock()
|
||||
|
||||
// begin transaction
|
||||
tx, err := database.sqlConn.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// delete tag
|
||||
_, err = tx.Stmt(database.stmt.deleteTag).Exec(id)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
// delete file_has_tag
|
||||
_, err = tx.Stmt(database.stmt.deleteTagReferenceInFileHasTag).Exec(id)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
// commit transaction
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -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,782 +0,0 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
)
|
||||
|
||||
var initFilesTableQuery = `CREATE TABLE IF NOT EXISTS files (
|
||||
id SERIAL PRIMARY KEY,
|
||||
folder_id INTEGER NOT NULL REFERENCES folders(id),
|
||||
realname TEXT NOT NULL,
|
||||
filename TEXT NOT NULL,
|
||||
filesize INTEGER NOT NULL,
|
||||
UNIQUE (folder_id, realname)
|
||||
);`
|
||||
|
||||
var initFoldersTableQuery = `CREATE TABLE IF NOT EXISTS folders (
|
||||
id SERIAL PRIMARY KEY,
|
||||
folder TEXT NOT NULL UNIQUE,
|
||||
foldername TEXT NOT NULL
|
||||
);`
|
||||
|
||||
var initFeedbacksTableQuery = `CREATE TABLE IF NOT EXISTS feedbacks (
|
||||
id SERIAL 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
|
||||
// postgres avatar references problem
|
||||
var initUsersTableQuery = `CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password TEXT NOT NULL,
|
||||
role INTEGER NOT NULL,
|
||||
active BOOLEAN NOT NULL,
|
||||
avatar_id INTEGER NOT NULL DEFAULT 0
|
||||
);`
|
||||
|
||||
var initAvatarsTableQuery = `CREATE TABLE IF NOT EXISTS avatars (
|
||||
id SERIAL PRIMARY KEY,
|
||||
avatarname TEXT NOT NULL,
|
||||
avatar BYTEA NOT NULL
|
||||
);`
|
||||
|
||||
var initTagsTableQuery = `CREATE TABLE IF NOT EXISTS tags (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT NOT NULL,
|
||||
created_by_user_id INTEGER NOT NULL REFERENCES users(id)
|
||||
);`
|
||||
|
||||
var initFileHasTagTableQuery = `CREATE TABLE IF NOT EXISTS file_has_tag (
|
||||
file_id INTEGER NOT NULL REFERENCES files(id),
|
||||
tag_id INTEGER NOT NULL REFERENCES tags(id),
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
PRIMARY KEY (file_id, tag_id)
|
||||
);`
|
||||
|
||||
var initLikesTableQuery = `CREATE TABLE IF NOT EXISTS likes (
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
file_id INTEGER NOT NULL REFERENCES files(id),
|
||||
PRIMARY KEY (user_id, file_id)
|
||||
);`
|
||||
|
||||
var initReviewsTableQuery = `CREATE TABLE IF NOT EXISTS reviews (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
file_id INTEGER NOT NULL REFERENCES files(id),
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL DEFAULT 0,
|
||||
content TEXT NOT NULL
|
||||
);`
|
||||
|
||||
var initPlaybacksTableQuery = `CREATE TABLE IF NOT EXISTS playbacks (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
file_id INTEGER NOT NULL REFERENCES files(id),
|
||||
time TIMESTAMP NOT NULL,
|
||||
method INTEGER NOT NULL,
|
||||
duration INTERVAL NOT NULL
|
||||
);`
|
||||
|
||||
var initLogsTableQuery = `CREATE TABLE IF NOT EXISTS logs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
time INTEGER NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id)
|
||||
);`
|
||||
|
||||
var initTmpfsTableQuery = `CREATE TABLE IF NOT EXISTS tmpfs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
path TEXT NOT NULL,
|
||||
size INTEGER NOT NULL,
|
||||
file_id INTEGER NOT NULL REFERENCES files(id),
|
||||
ffmpeg_config TEXT NOT NULL,
|
||||
created_time INTEGER NOT NULL,
|
||||
accessed_time INTEGER NOT NULL
|
||||
);`
|
||||
|
||||
var insertFolderQuery = `INSERT INTO folders (folder, foldername)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT DO NOTHING
|
||||
RETURNING id;
|
||||
;`
|
||||
|
||||
var findFolderQuery = `SELECT id FROM folders WHERE folder = $1 LIMIT 1;`
|
||||
|
||||
var findFileQuery = `SELECT id FROM files WHERE folder_id = $1 AND realname = $2 LIMIT 1;`
|
||||
|
||||
var insertFileQuery = `INSERT INTO files (folder_id, realname, filename, filesize)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT DO NOTHING
|
||||
RETURNING id;`
|
||||
|
||||
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 ILIKE $1
|
||||
ORDER BY folders.foldername, files.filename
|
||||
LIMIT $2 OFFSET $3;`
|
||||
|
||||
var getFolderQuery = `SELECT folder FROM folders WHERE id = $1 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 = $1
|
||||
LIMIT 1;`
|
||||
|
||||
var searchFoldersQuery = `SELECT
|
||||
id, folder, foldername
|
||||
FROM folders
|
||||
WHERE foldername ILIKE $1
|
||||
ORDER BY foldername
|
||||
LIMIT $2 OFFSET $3;`
|
||||
|
||||
var getFilesInFolderQuery = `SELECT
|
||||
files.id, files.filename, files.filesize, folders.foldername, folders.folder
|
||||
FROM files
|
||||
JOIN folders ON files.folder_id = folders.id
|
||||
WHERE folder_id = $1
|
||||
ORDER BY files.filename
|
||||
LIMIT $2 OFFSET $3;`
|
||||
|
||||
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 $1;`
|
||||
|
||||
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 = $1
|
||||
ORDER BY RANDOM()
|
||||
LIMIT $2;`
|
||||
|
||||
var insertFeedbackQuery = `INSERT INTO feedbacks (time, content, user_id, header)
|
||||
VALUES ($1, $2, $3, $4);`
|
||||
|
||||
var getFeedbacksQuery = `SELECT
|
||||
feedbacks.id, feedbacks.time, feedbacks.content, feedbacks.header,
|
||||
users.id, users.username, users.role, users.active, users.avatar_id
|
||||
FROM feedbacks
|
||||
JOIN users ON feedbacks.user_id = users.id
|
||||
ORDER BY feedbacks.time
|
||||
;`
|
||||
|
||||
var deleteFeedbackQuery = `DELETE FROM feedbacks WHERE id = $1;`
|
||||
|
||||
var insertUserQuery = `INSERT INTO users (username, password, role, active, avatar_id)
|
||||
VALUES ($1, $2, $3, $4, $5);`
|
||||
|
||||
var countUserQuery = `SELECT count(*) FROM users;`
|
||||
|
||||
var countAdminQuery = `SELECT count(*) FROM users WHERE role= 1;`
|
||||
|
||||
var getUserQuery = `SELECT id, username, password, role, active, avatar_id FROM users WHERE username = $1 LIMIT 1;`
|
||||
|
||||
var getUsersQuery = `SELECT id, username, role, active, avatar_id FROM users;`
|
||||
|
||||
var getUserByIdQuery = `SELECT id, username, role, active, avatar_id FROM users WHERE id = $1 LIMIT 1;`
|
||||
|
||||
var updateUserActiveQuery = `UPDATE users SET active = $1 WHERE id = $2;`
|
||||
|
||||
var updateUsernameQuery = `UPDATE users SET username = $1 WHERE id = $2;`
|
||||
|
||||
var updateUserPasswordQuery = `UPDATE users SET password = $1 WHERE id = $2;`
|
||||
|
||||
var getAnonymousUserQuery = `SELECT id, username, role, avatar_id FROM users WHERE role = 0 LIMIT 1;`
|
||||
|
||||
var insertTagQuery = `INSERT INTO tags (name, description, created_by_user_id) VALUES ($1, $2, $3) RETURNING id;`
|
||||
|
||||
var deleteTagQuery = `DELETE FROM tags WHERE id = $1;`
|
||||
|
||||
var getTagQuery = `SELECT
|
||||
tags.id, tags.name, tags.description,
|
||||
users.id, users.username, users.role, users.avatar_id
|
||||
FROM tags
|
||||
JOIN users ON tags.created_by_user_id = users.id
|
||||
WHERE tags.id = $1 LIMIT 1;`
|
||||
|
||||
var getTagsQuery = `SELECT
|
||||
tags.id, tags.name, tags.description,
|
||||
users.id, users.username, users.role, users.avatar_id
|
||||
FROM tags
|
||||
JOIN users ON tags.created_by_user_id = users.id
|
||||
ORDER BY tags.name
|
||||
;`
|
||||
|
||||
var updateTagQuery = `UPDATE tags SET name = $1, description = $2 WHERE id = $3;`
|
||||
|
||||
// postgres INSERT IGNORE
|
||||
var putTagOnFileQuery = `INSERT INTO file_has_tag (tag_id, file_id, user_id) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING;`
|
||||
|
||||
var getTagsOnFileQuery = `SELECT
|
||||
tags.id, tags.name, tags.description, tags.created_by_user_id
|
||||
FROM file_has_tag
|
||||
JOIN tags ON file_has_tag.tag_id = tags.id
|
||||
WHERE file_has_tag.file_id = $1
|
||||
ORDER BY tags.name
|
||||
;`
|
||||
|
||||
var deleteTagOnFileQuery = `DELETE FROM file_has_tag WHERE tag_id = $1 AND file_id = $2;`
|
||||
|
||||
var deleteTagReferenceInFileHasTagQuery = `DELETE FROM file_has_tag WHERE tag_id = $1;`
|
||||
|
||||
var updateFoldernameQuery = `UPDATE folders SET foldername = $1 WHERE id = $2;`
|
||||
|
||||
var insertReviewQuery = `INSERT INTO reviews (user_id, file_id, created_at, content)
|
||||
VALUES ($1, $2, $3, $4);`
|
||||
|
||||
var getReviewsOnFileQuery = `SELECT
|
||||
reviews.id, reviews.created_at, reviews.updated_at, reviews.content,
|
||||
users.id, users.username, users.role, users.avatar_id,
|
||||
files.id, files.filename
|
||||
FROM reviews
|
||||
JOIN users ON reviews.user_id = users.id
|
||||
JOIN files ON reviews.file_id = files.id
|
||||
WHERE reviews.file_id = $1
|
||||
ORDER BY reviews.created_at
|
||||
;`
|
||||
|
||||
var getReviewQuery = `SELECT id, file_id, user_id, created_at, updated_at, content FROM reviews WHERE id = $1 LIMIT 1;`
|
||||
|
||||
var updateReviewQuery = `UPDATE reviews SET content = $1, updated_at = $2 WHERE id = $3;`
|
||||
|
||||
var deleteReviewQuery = `DELETE FROM reviews WHERE id = $1;`
|
||||
|
||||
var getReviewsByUserQuery = `SELECT
|
||||
reviews.id, reviews.created_at, reviews.updated_at, reviews.content,
|
||||
users.id, users.username, users.role, users.avatar_id,
|
||||
files.id, files.filename
|
||||
FROM reviews
|
||||
JOIN users ON reviews.user_id = users.id
|
||||
JOIN files ON reviews.file_id = files.id
|
||||
WHERE reviews.user_id = $1
|
||||
ORDER BY reviews.created_at
|
||||
;`
|
||||
|
||||
var deleteFileQuery = `DELETE FROM files WHERE id = $1;`
|
||||
|
||||
var deleteFileReferenceInFileHasTagQuery = `DELETE FROM file_has_tag WHERE file_id = $1;`
|
||||
|
||||
var deleteFileReferenceInReviewsQuery = `DELETE FROM reviews WHERE file_id = $1;`
|
||||
|
||||
var updateFilenameQuery = `UPDATE files SET filename = $1 WHERE id = $2;`
|
||||
|
||||
var resetFilenameQuery = `UPDATE files SET filename = realname WHERE id = $1;`
|
||||
|
||||
var recordPlaybackQuery = `INSERT INTO playbacks (user_id, file_id, time, method, duration) VALUES ($1, $2, $3, $4, $5);`
|
||||
|
||||
type Stmt struct {
|
||||
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
|
||||
recordPlaybackStmt *sql.Stmt
|
||||
}
|
||||
|
||||
func NewPreparedStatement(sqlConn *sql.DB) (*Stmt, error) {
|
||||
var err error
|
||||
|
||||
stmt := &Stmt{}
|
||||
|
||||
// init folders table
|
||||
stmt.initFoldersTable, err = sqlConn.Prepare(initFoldersTableQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = stmt.initFoldersTable.Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init files table
|
||||
stmt.initFilesTable, err = sqlConn.Prepare(initFilesTableQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = stmt.initFilesTable.Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init avatars table
|
||||
stmt.initAvatarsTable, err = sqlConn.Prepare(initAvatarsTableQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = stmt.initAvatarsTable.Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init users table
|
||||
stmt.initUsersTable, err = sqlConn.Prepare(initUsersTableQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = stmt.initUsersTable.Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init feedbacks tables
|
||||
stmt.initFeedbacksTable, err = sqlConn.Prepare(initFeedbacksTableQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = stmt.initFeedbacksTable.Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init tags table
|
||||
stmt.initTagsTable, err = sqlConn.Prepare(initTagsTableQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = stmt.initTagsTable.Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init file_has_tag table
|
||||
stmt.initFileHasTag, err = sqlConn.Prepare(initFileHasTagTableQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = stmt.initFileHasTag.Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init likes table
|
||||
stmt.initLikesTable, err = sqlConn.Prepare(initLikesTableQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = stmt.initLikesTable.Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init reviews table
|
||||
stmt.initReviewsTable, err = sqlConn.Prepare(initReviewsTableQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = stmt.initReviewsTable.Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init playbacks table
|
||||
stmt.initPlaybacksTable, err = sqlConn.Prepare(initPlaybacksTableQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = stmt.initPlaybacksTable.Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init logs table
|
||||
stmt.initLogsTable, err = sqlConn.Prepare(initLogsTableQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = stmt.initLogsTable.Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// init tmpfs table
|
||||
stmt.initTmpfsTable, err = sqlConn.Prepare(initTmpfsTableQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = stmt.initTmpfsTable.Exec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Println("Init tables finished")
|
||||
|
||||
// init insert folder statement
|
||||
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
|
||||
}
|
||||
|
||||
stmt.recordPlaybackStmt, err = sqlConn.Prepare(recordPlaybackQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Println("Init statements finished")
|
||||
|
||||
return stmt, err
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
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"`
|
||||
folderCache *Folder
|
||||
}
|
||||
|
||||
type Folder struct {
|
||||
Db *Database `json:"-"`
|
||||
ID int64 `json:"id"`
|
||||
Folder string `json:"folder"`
|
||||
Foldername string `json:"foldername"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID int64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"-"`
|
||||
Role int64 `json:"role"`
|
||||
Active bool `json:"active"`
|
||||
AvatarId int64 `json:"avatar_id"`
|
||||
}
|
||||
|
||||
type Tag struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
CreatedByUserId int64 `json:"created_by_user_id"`
|
||||
CreatedByUser *User `json:"created_by_user"`
|
||||
}
|
||||
|
||||
type Review struct {
|
||||
ID int64 `json:"id"`
|
||||
FileId int64 `json:"file_id"`
|
||||
File *File `json:"file"`
|
||||
UserId int64 `json:"user_id"`
|
||||
User *User `json:"user"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type Feedback struct {
|
||||
ID int64 `json:"id"`
|
||||
UserId int64 `json:"user_id"`
|
||||
User *User `json:"user"`
|
||||
Content string `json:"content"`
|
||||
Header string `json:"header"`
|
||||
Time int64 `json:"time"`
|
||||
}
|
||||
|
||||
type Playback struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
FileID int64 `json:"file_id"`
|
||||
Time time.Time `json:"time"`
|
||||
Method int64 `json:"method"`
|
||||
Duration time.Duration `json:"Duration"`
|
||||
}
|
||||
|
||||
var (
|
||||
RoleAnonymous = int64(0)
|
||||
RoleAdmin = int64(1)
|
||||
RoleUser = int64(2)
|
||||
)
|
||||
|
||||
func (f *File) Path() (string, error) {
|
||||
var err error
|
||||
if f.folderCache == nil {
|
||||
f.folderCache, err = f.Db.GetFolder(f.Folder_id)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(f.folderCache.Folder, f.Realname), nil
|
||||
}
|
||||
|
||||
func (f *File) Dir() (string, error) {
|
||||
var err error
|
||||
if f.folderCache == nil {
|
||||
f.folderCache, err = f.Db.GetFolder(f.Folder_id)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return f.folderCache.Folder, nil
|
||||
}
|
||||
@@ -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,9 +1,58 @@
|
||||
# MSW Open Music Web Frontend
|
||||
# msw-open-music web font-end
|
||||
|
||||
This is a React single page application. And use Preact instead of React to achieve a smaller file size.
|
||||
This msw-open-music project was bootstrapped with `Create React App`
|
||||
|
||||
`node_modules` only has 19M. We uses esbuild and shell scripts and build only takes a milliseconds!
|
||||
## Group 9 information
|
||||
|
||||
## How to build
|
||||
| Name | Name (EN) | No |
|
||||
| ------ | ------------- | ---------- |
|
||||
| 陈永源 | CHEN Yongyuan | 1930006025 |
|
||||
| 鲁雷 | Lu Lei | 2030026101 |
|
||||
| 张滨玮 | Zhang Binwei | 2030026197 |
|
||||
| 丁俊超 | Ding Junchao | 2030026258 |
|
||||
| 邱星越 | Qiu Xingyue | 2030026119 |
|
||||
| 李真晔 | Li Zhenye | 2030006104 |
|
||||
|
||||
Simple run `./build.sh`, then all output files are under `./build/` directory.
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
rm -rf build
|
||||
cp -raf public build
|
||||
./node_modules/.bin/esbuild src/index.jsx --bundle --outfile=build/msw-open-music.js --alias:react=preact/compat --alias:react-dom=preact/compat --minify --analyze
|
||||
cat public/index.html | sed "s/%PUBLIC_URL%/$PUBLIC_URL/" > build/index.html
|
||||
|
||||
echo "Build done, output files under ./build directory"
|
||||
37870
web/package-lock.json
generated
37870
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,44 @@
|
||||
{
|
||||
"name": "msw-open-music-react",
|
||||
"version": "1.2.0",
|
||||
"version": "1.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@preact/compat": "^17.1.2",
|
||||
"esbuild": "^0.15.17",
|
||||
"react-router-dom": "^6.4.4",
|
||||
"water.css": "^2.1.1"
|
||||
"@testing-library/jest-dom": "^5.15.0",
|
||||
"@testing-library/react": "^11.2.7",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-router": "^6.0.2",
|
||||
"react-router-dom": "^6.0.2",
|
||||
"react-scripts": "4.0.3",
|
||||
"water.css": "^2.1.1",
|
||||
"web-vitals": "^1.1.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "bash ./build.sh"
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^17.0.34"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,36 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="Personal music streaming platform" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/favicon.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<link rel="stylesheet" href="%PUBLIC_URL%/msw-open-music.css" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<!-- Add to homescreen for Chrome on Android -->
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<title>MSW Open Music</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="%PUBLIC_URL%/msw-open-music.js"></script>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,19 +3,11 @@ html {
|
||||
}
|
||||
body {
|
||||
margin: auto;
|
||||
padding-top: 1rem;
|
||||
max-width: unset;
|
||||
min-height: 100vh;
|
||||
}
|
||||
#root {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.base {
|
||||
display: grid;
|
||||
grid-row-gap: 1em;
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
}
|
||||
.header {
|
||||
color: white;
|
||||
@@ -23,13 +15,18 @@ body {
|
||||
box-shadow: 0 0 8px #393939;
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
.avatar {
|
||||
border-radius: 50%;
|
||||
background-color: lightpink;
|
||||
padding: 0.39rem;
|
||||
}
|
||||
.title {
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
vertical-align: middle;
|
||||
justify-content: space-between;
|
||||
color: white;
|
||||
}
|
||||
.title-text {
|
||||
margin-left: 1em;
|
||||
@@ -103,19 +100,3 @@ dialog {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.horizontal {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.vertical {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.warp-word {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.number-input {
|
||||
width: 5em;
|
||||
}
|
||||
|
||||
91
web/src/App.js
Normal file
91
web/src/App.js
Normal file
@@ -0,0 +1,91 @@
|
||||
import { HashRouter as Router, Routes, Route, NavLink } from "react-router-dom";
|
||||
import "./App.css";
|
||||
|
||||
import GetRandomFiles from "./component/GetRandomFiles";
|
||||
import SearchFiles from "./component/SearchFiles";
|
||||
import SearchFolders from "./component/SearchFolders";
|
||||
import Manage from "./component/Manage";
|
||||
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 Register from "./component/Register";
|
||||
import { useState } from "react";
|
||||
|
||||
function App() {
|
||||
const [playingFile, setPlayingFile] = useState({});
|
||||
const [user, setUser] = useState(null);
|
||||
return (
|
||||
<div className="base">
|
||||
<Router>
|
||||
<header className="header">
|
||||
<h3 className="title">
|
||||
<img src="favicon.png" alt="logo" className="logo" />
|
||||
<span className="title-text">MSW Open Music Project</span>
|
||||
<User user={user} setUser={setUser} />
|
||||
</h3>
|
||||
<nav className="nav">
|
||||
<NavLink to="/" className="nav-link">
|
||||
Feeling luckly
|
||||
</NavLink>
|
||||
<NavLink to="/search-files" className="nav-link">
|
||||
Files
|
||||
</NavLink>
|
||||
<NavLink to="/search-folders" className="nav-link">
|
||||
Folders
|
||||
</NavLink>
|
||||
<NavLink to="/manage" className="nav-link">
|
||||
Manage
|
||||
</NavLink>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route
|
||||
index
|
||||
path="/"
|
||||
element={<GetRandomFiles setPlayingFile={setPlayingFile} />}
|
||||
/>
|
||||
<Route
|
||||
path="/search-files"
|
||||
element={<SearchFiles setPlayingFile={setPlayingFile} />}
|
||||
/>
|
||||
<Route
|
||||
path="/search-folders"
|
||||
element={<SearchFolders setPlayingFile={setPlayingFile} />}
|
||||
/>
|
||||
<Route
|
||||
path="/folder/:id"
|
||||
element={<FilesInFolder setPlayingFile={setPlayingFile} />}
|
||||
/>
|
||||
<Route path="/manage" element={<Manage />} />
|
||||
<Route
|
||||
path="/file/:id/share"
|
||||
element={<Share setPlayingFile={setPlayingFile} />}
|
||||
/>
|
||||
<Route path="/file/:id/review" element={<Review />} />
|
||||
<Route
|
||||
path="/profile/:id"
|
||||
element={<Profile user={user} setUser={setUser} />}
|
||||
/>
|
||||
<Route path="/login" element={<Login setUser={setUser} />} />
|
||||
<Route path="/register" element={<Register setUser={setUser} />} />
|
||||
<Route path="/file/:id" element={<FileInfo />} />
|
||||
</Routes>
|
||||
</main>
|
||||
<footer>
|
||||
<AudioPlayer
|
||||
playingFile={playingFile}
|
||||
setPlayingFile={setPlayingFile}
|
||||
/>
|
||||
</footer>
|
||||
</Router>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
177
web/src/App.jsx
177
web/src/App.jsx
@@ -1,177 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { HashRouter as Router, Routes, Route, NavLink } from "react-router-dom";
|
||||
import "./App.css";
|
||||
|
||||
import GetRandomFiles from "./component/GetRandomFiles";
|
||||
import SearchFiles from "./component/SearchFiles";
|
||||
import SearchFolders from "./component/SearchFolders";
|
||||
import FilesInFolder from "./component/FilesInFolder";
|
||||
import Manage from "./component/Manage";
|
||||
import ManageUser from "./component/ManageUser";
|
||||
import FileInfo from "./component/FileInfo";
|
||||
import Share from "./component/Share";
|
||||
import Login from "./component/Login";
|
||||
import Register from "./component/Register";
|
||||
import Tags from "./component/Tags";
|
||||
import EditTag from "./component/EditTag";
|
||||
import EditReview from "./component/EditReview";
|
||||
import AudioPlayer from "./component/AudioPlayer";
|
||||
import UserStatus from "./component/UserStatus";
|
||||
import ReviewPage from "./component/ReviewPage";
|
||||
import UserProfile from "./component/UserProfile";
|
||||
import FeedbackPage from "./component/FeedbackPage";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Tr, langCodeContext, LANG_OPTIONS } from "./translate";
|
||||
|
||||
function App() {
|
||||
const [playingFile, setPlayingFile] = useState({});
|
||||
const [user, setUser] = useState({});
|
||||
const [langCode, setLangCode] = useState("en_US");
|
||||
|
||||
useEffect(() => {
|
||||
if (playingFile.id === undefined) {
|
||||
return;
|
||||
}
|
||||
const html = document.getElementsByTagName("html")[0];
|
||||
const retStyle = html.style;
|
||||
const bodyRetStyle = document.body.style
|
||||
html.style = `
|
||||
backdrop-filter: blur(10px);
|
||||
background-size: cover;
|
||||
background-attachment: fixed;
|
||||
background-position: center;
|
||||
background-image: url("/api/v1/get_file_avatar?id=${playingFile.id}");
|
||||
`;
|
||||
document.body.style.opacity = 0.88;
|
||||
return () => {
|
||||
html.style = retStyle;
|
||||
document.body.style = bodyRetStyle;
|
||||
};
|
||||
}, [playingFile.id]);
|
||||
|
||||
// select language
|
||||
useEffect(() => {
|
||||
const browserCode = window.navigator.language;
|
||||
for (const key in LANG_OPTIONS) {
|
||||
for (const i in LANG_OPTIONS[key].matches) {
|
||||
const code = LANG_OPTIONS[key].matches[i];
|
||||
if (code === browserCode) {
|
||||
setLangCode(key);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// fallback to english
|
||||
setLangCode("en-US");
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="base">
|
||||
<langCodeContext.Provider value={{ langCode, setLangCode }}>
|
||||
<Router>
|
||||
<header className="header">
|
||||
<h3 className="title">
|
||||
<img src="favicon.png" alt="logo" className="logo" />
|
||||
<span className="title-text">MSW Open Music Project</span>
|
||||
<UserStatus user={user} setUser={setUser} />
|
||||
</h3>
|
||||
<nav className="nav">
|
||||
<NavLink to="/" className="nav-link">
|
||||
{Tr("Feeling luckly")}
|
||||
</NavLink>
|
||||
<NavLink to="/files" className="nav-link">
|
||||
{Tr("Files")}
|
||||
</NavLink>
|
||||
<NavLink to="/folders" className="nav-link">
|
||||
{Tr("Folders")}
|
||||
</NavLink>
|
||||
<NavLink to="/manage" className="nav-link">
|
||||
{Tr("Manage")}
|
||||
</NavLink>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<Routes>
|
||||
<Route
|
||||
index
|
||||
path="/"
|
||||
element={<GetRandomFiles setPlayingFile={setPlayingFile} />}
|
||||
/>
|
||||
<Route
|
||||
path="/files"
|
||||
element={<SearchFiles setPlayingFile={setPlayingFile} />}
|
||||
/>
|
||||
<Route
|
||||
path="/folders"
|
||||
element={<SearchFolders setPlayingFile={setPlayingFile} />}
|
||||
/>
|
||||
<Route
|
||||
path="/folders/:id"
|
||||
element={<FilesInFolder setPlayingFile={setPlayingFile} />}
|
||||
/>
|
||||
<Route
|
||||
path="/manage"
|
||||
element={
|
||||
<Manage
|
||||
user={user}
|
||||
setUser={setUser}
|
||||
setLangCode={setLangCode}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/manage/feedbacks"
|
||||
element={<FeedbackPage user={user} />}
|
||||
/>
|
||||
<Route
|
||||
path="/manage/login"
|
||||
element={<Login user={user} setUser={setUser} />}
|
||||
/>
|
||||
<Route
|
||||
path="/manage/register"
|
||||
element={<Register user={user} setUser={setUser} />}
|
||||
/>
|
||||
<Route path="/manage/tags" element={<Tags user={user} />} />
|
||||
<Route
|
||||
path="/manage/tags/:id"
|
||||
element={<EditTag user={user} />}
|
||||
/>
|
||||
<Route
|
||||
path="/manage/reviews/:id"
|
||||
element={<EditReview user={user} />}
|
||||
/>
|
||||
<Route
|
||||
path="/manage/users"
|
||||
element={<ManageUser user={user} setUser={setUser} />}
|
||||
/>
|
||||
<Route
|
||||
path="/manage/users/:id"
|
||||
element={<UserProfile user={user} setUser={setUser} />}
|
||||
/>
|
||||
<Route
|
||||
path="/files/:id"
|
||||
element={<FileInfo setPlayingFile={setPlayingFile} />}
|
||||
/>
|
||||
<Route
|
||||
path="/files/:id/share"
|
||||
element={<Share setPlayingFile={setPlayingFile} />}
|
||||
/>
|
||||
<Route
|
||||
path="/files/:id/review"
|
||||
element={
|
||||
<ReviewPage user={user} setPlayingFile={setPlayingFile} />
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</main>
|
||||
<AudioPlayer
|
||||
playingFile={playingFile}
|
||||
setPlayingFile={setPlayingFile}
|
||||
/>
|
||||
</Router>
|
||||
</langCodeContext.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
154
web/src/component/AudioPlayer.js
Normal file
154
web/src/component/AudioPlayer.js
Normal file
@@ -0,0 +1,154 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { CalcReadableFilesizeDetail } from "./Common";
|
||||
import FfmpegConfig from "./FfmpegConfig";
|
||||
import FileDialog from "./FileDialog";
|
||||
|
||||
function AudioPlayer(props) {
|
||||
// props.playingFile
|
||||
// props.setPlayingFile
|
||||
|
||||
const [fileDialogShowStatus, setFileDialogShowStatus] = useState(false);
|
||||
const [loop, setLoop] = useState(true);
|
||||
const [raw, setRaw] = useState(false);
|
||||
const [prepare, setPrepare] = useState(false);
|
||||
const [selectedFfmpegConfig, setSelectedFfmpegConfig] = useState({});
|
||||
const [playingURL, setPlayingURL] = useState("");
|
||||
const [isPreparing, setIsPreparing] = useState(false);
|
||||
const [preparedFilesize, setPreparedFilesize] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
// no playing file
|
||||
if (props.playingFile.id === undefined) {
|
||||
setPlayingURL("");
|
||||
return;
|
||||
}
|
||||
if (raw) {
|
||||
console.log("Play raw file");
|
||||
setPlayingURL("/api/v1/get_file_direct?id=" + props.playingFile.id);
|
||||
} else {
|
||||
if (prepare) {
|
||||
// prepare file
|
||||
setIsPreparing(true);
|
||||
fetch("/api/v1/prepare_file_stream_direct", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
id: props.playingFile.id,
|
||||
config_name: selectedFfmpegConfig.name,
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
setPreparedFilesize(data.filesize);
|
||||
setIsPreparing(false);
|
||||
setPlayingURL(
|
||||
`/api/v1/get_file_stream_direct?id=${props.playingFile.id}&config=${selectedFfmpegConfig.name}`
|
||||
);
|
||||
});
|
||||
} else {
|
||||
setPlayingURL(
|
||||
`/api/v1/get_file_stream?id=${props.playingFile.id}&config=${selectedFfmpegConfig.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [props.playingFile.id, raw, prepare, selectedFfmpegConfig]);
|
||||
|
||||
let navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h5>Player status</h5>
|
||||
{props.playingFile.id && (
|
||||
<span>
|
||||
<FileDialog
|
||||
showStatus={fileDialogShowStatus}
|
||||
setShowStatus={setFileDialogShowStatus}
|
||||
file={props.playingFile}
|
||||
setPlayingFile={() => {
|
||||
return;
|
||||
}}
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setFileDialogShowStatus(!fileDialogShowStatus);
|
||||
}}
|
||||
>
|
||||
{props.playingFile.filename}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() =>
|
||||
navigate(`search-folders/${props.playingFile.folder_id}`)
|
||||
}
|
||||
>
|
||||
{props.playingFile.foldername}
|
||||
</button>
|
||||
|
||||
<button disabled>
|
||||
{prepare
|
||||
? CalcReadableFilesizeDetail(preparedFilesize)
|
||||
: CalcReadableFilesizeDetail(props.playingFile.filesize)}
|
||||
</button>
|
||||
|
||||
{isPreparing && <button disabled>Preparing...</button>}
|
||||
|
||||
{playingURL !== "" && (
|
||||
<button
|
||||
onClick={() => {
|
||||
props.setPlayingFile({});
|
||||
}}
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<br />
|
||||
|
||||
<input
|
||||
checked={loop}
|
||||
onChange={(event) => setLoop(event.target.checked)}
|
||||
type="checkbox"
|
||||
/>
|
||||
<label>Loop</label>
|
||||
|
||||
<input
|
||||
checked={raw}
|
||||
onChange={(event) => setRaw(event.target.checked)}
|
||||
type="checkbox"
|
||||
/>
|
||||
<label>Raw</label>
|
||||
|
||||
{!raw && (
|
||||
<span>
|
||||
<input
|
||||
checked={prepare}
|
||||
onChange={(event) => setPrepare(event.target.checked)}
|
||||
type="checkbox"
|
||||
/>
|
||||
<label>Prepare</label>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{playingURL !== "" && (
|
||||
<audio
|
||||
controls
|
||||
autoPlay
|
||||
loop={loop}
|
||||
className="audio-player"
|
||||
src={playingURL}
|
||||
></audio>
|
||||
)}
|
||||
|
||||
<FfmpegConfig
|
||||
selectedFfmpegConfig={selectedFfmpegConfig}
|
||||
setSelectedFfmpegConfig={setSelectedFfmpegConfig}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AudioPlayer;
|
||||
@@ -1,275 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import {useEffect, useState} from "react";
|
||||
import {useNavigate} from "react-router";
|
||||
import {CalcReadableFilesizeDetail} from "./Common";
|
||||
import FfmpegConfig from "./FfmpegConfig";
|
||||
import FileDialog from "./FileDialog";
|
||||
import {Tr} from "../translate";
|
||||
|
||||
function AudioPlayer(props) {
|
||||
// props.playingFile
|
||||
// props.setPlayingFile
|
||||
|
||||
const [fileDialogShowStatus, setFileDialogShowStatus] = useState(false);
|
||||
const [loop, setLoop] = useState(true);
|
||||
const [raw, setRaw] = useState(false);
|
||||
const [prepare, setPrepare] = useState(false);
|
||||
const [selectedFfmpegConfig, setSelectedFfmpegConfig] = useState({
|
||||
name: "",
|
||||
args: "",
|
||||
});
|
||||
const [playingURL, setPlayingURL] = useState("");
|
||||
const [isPreparing, setIsPreparing] = useState(false);
|
||||
const [timerCount, setTimerCount] = useState(0);
|
||||
const [timerID, setTimerID] = useState(null);
|
||||
const [beginPlayTime, setBeginPlayTime] = useState(null);
|
||||
const [lastID, setLastID] = useState(null);
|
||||
|
||||
const recordPlaybackHistory = async (file_id, method) => {
|
||||
if (file_id === null) {
|
||||
return
|
||||
}
|
||||
const player = document.getElementById('dom-player')
|
||||
const endPlayTime = new Date()
|
||||
let duration = parseInt((endPlayTime - beginPlayTime) / 1000)
|
||||
const maxDuration = parseInt(player.duration)
|
||||
// treat 85% of duration as finished
|
||||
if (duration / maxDuration >= 0.85) {
|
||||
method = 1
|
||||
}
|
||||
duration = duration < maxDuration ? duration : maxDuration
|
||||
setBeginPlayTime(endPlayTime)
|
||||
await fetch('/api/v1/record_playback', {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
playback: {
|
||||
file_id,
|
||||
method,
|
||||
duration,
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// init mediaSession API
|
||||
useEffect(() => {
|
||||
if (navigator.mediaSession) {
|
||||
navigator.mediaSession.setActionHandler("stop", () => {
|
||||
props.setPlayingFile({});
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const updatePlayMode = () => {
|
||||
if (props.playingFile.id === undefined) {
|
||||
return
|
||||
}
|
||||
if (raw) {
|
||||
console.log("Play raw file");
|
||||
setPlayingURL("/api/v1/get_file_direct?id=" + props.playingFile.id);
|
||||
} else {
|
||||
if (prepare) {
|
||||
// prepare file
|
||||
setIsPreparing(true);
|
||||
fetch("/api/v1/prepare_file_stream_direct", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({
|
||||
id: props.playingFile.id,
|
||||
config_name: selectedFfmpegConfig.name,
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
setIsPreparing(false);
|
||||
return;
|
||||
}
|
||||
props.setPlayingFile(data.file);
|
||||
setIsPreparing(false);
|
||||
setPlayingURL(
|
||||
`/api/v1/get_file_stream_direct?id=${props.playingFile.id}&config=${selectedFfmpegConfig.name}`
|
||||
);
|
||||
});
|
||||
} else {
|
||||
setPlayingURL(
|
||||
`/api/v1/get_file_stream?id=${props.playingFile.id}&config=${selectedFfmpegConfig.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// media session related staff
|
||||
if (navigator.mediaSession) {
|
||||
navigator.mediaSession.metadata = new window.MediaMetadata({
|
||||
title: props.playingFile.filename,
|
||||
album: props.playingFile.foldername,
|
||||
artwork: [{src: "/favicon.png", type: "image/png"}],
|
||||
});
|
||||
}
|
||||
// no playing file
|
||||
if (props.playingFile.id === undefined) {
|
||||
// 3 music stopped
|
||||
recordPlaybackHistory(lastID, 3)
|
||||
setPlayingURL("");
|
||||
return;
|
||||
}
|
||||
// crrently playing file, record interupt
|
||||
if (playingURL) {
|
||||
// 2 music changed
|
||||
recordPlaybackHistory(lastID, 2)
|
||||
}
|
||||
setLastID(props.playingFile.id)
|
||||
// have playingFile, record begin time
|
||||
setBeginPlayTime(new Date())
|
||||
updatePlayMode()
|
||||
}, [props.playingFile.id]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
updatePlayMode()
|
||||
}, [raw, prepare, selectedFfmpegConfig])
|
||||
|
||||
let navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<footer className="vertical">
|
||||
<h5>{Tr("Player status")}</h5>
|
||||
{props.playingFile.id && (
|
||||
<span>
|
||||
<FileDialog
|
||||
showStatus={fileDialogShowStatus}
|
||||
setShowStatus={setFileDialogShowStatus}
|
||||
file={props.playingFile}
|
||||
setPlayingFile={() => {
|
||||
return;
|
||||
}}
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setFileDialogShowStatus(!fileDialogShowStatus);
|
||||
}}
|
||||
>
|
||||
{props.playingFile.filename}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => navigate(`/folders/${props.playingFile.folder_id}`)}
|
||||
>
|
||||
{props.playingFile.foldername}
|
||||
</button>
|
||||
|
||||
<button disabled>
|
||||
{CalcReadableFilesizeDetail(props.playingFile.filesize)}
|
||||
</button>
|
||||
|
||||
{isPreparing && <button disabled>{Tr("Preparing...")}</button>}
|
||||
|
||||
{playingURL !== "" && (
|
||||
<button
|
||||
onClick={() => {
|
||||
props.setPlayingFile({});
|
||||
}}
|
||||
>
|
||||
{Tr("Stop")}
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<br />
|
||||
|
||||
<span className="horizontal">
|
||||
<input
|
||||
className="number-input"
|
||||
disabled={timerID !== null}
|
||||
type="number"
|
||||
value={timerCount}
|
||||
onChange={(e) => {
|
||||
setTimerCount(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (timerID != null) {
|
||||
clearInterval(timerID);
|
||||
setTimerID(null);
|
||||
return;
|
||||
}
|
||||
setTimerID(
|
||||
setTimeout(() => {
|
||||
props.setPlayingFile({});
|
||||
setTimerID(null);
|
||||
}, timerCount * 1000 * 60)
|
||||
);
|
||||
}}
|
||||
>
|
||||
{Tr("Stop Timer")}
|
||||
</button>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<span>
|
||||
<input
|
||||
checked={loop}
|
||||
onChange={(event) => setLoop(event.target.checked)}
|
||||
type="checkbox"
|
||||
/>
|
||||
<label>{Tr("Loop")}</label>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<input
|
||||
checked={raw}
|
||||
onChange={(event) => setRaw(event.target.checked)}
|
||||
type="checkbox"
|
||||
/>
|
||||
<label>{Tr("Raw")}</label>
|
||||
</span>
|
||||
|
||||
{!raw && (
|
||||
<span>
|
||||
<input
|
||||
checked={prepare}
|
||||
onChange={(event) => setPrepare(event.target.checked)}
|
||||
type="checkbox"
|
||||
/>
|
||||
<label>{Tr("Prepare")}</label>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
<audio
|
||||
id="dom-player"
|
||||
controls
|
||||
autoPlay
|
||||
className="audio-player"
|
||||
src={playingURL}
|
||||
onEnded={async () => {
|
||||
const player = document.getElementById('dom-player')
|
||||
if (loop) {
|
||||
player.play()
|
||||
}
|
||||
// 1 music finished
|
||||
recordPlaybackHistory(props.playingFile.id, 1)
|
||||
}}
|
||||
onPlay={async () => {
|
||||
setBeginPlayTime(new Date());
|
||||
}}
|
||||
></audio>
|
||||
|
||||
<FfmpegConfig
|
||||
selectedFfmpegConfig={selectedFfmpegConfig}
|
||||
setSelectedFfmpegConfig={setSelectedFfmpegConfig}
|
||||
/>
|
||||
</footer >
|
||||
);
|
||||
}
|
||||
|
||||
export default AudioPlayer;
|
||||
@@ -1,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) {
|
||||
if (filesize < 1024) {
|
||||
return filesize;
|
||||
@@ -43,30 +35,6 @@ function numberWithCommas(x) {
|
||||
return x;
|
||||
}
|
||||
|
||||
// convert unix timestamp to %Y-%m-%d %H:%M:%S
|
||||
export function convertIntToDateTime(timestamp) {
|
||||
var date = new Date(timestamp * 1000);
|
||||
var year = date.getFullYear();
|
||||
var month = date.getMonth() + 1;
|
||||
var day = date.getDate();
|
||||
var hour = date.getHours();
|
||||
var minute = date.getMinutes();
|
||||
var second = date.getSeconds();
|
||||
var time =
|
||||
year +
|
||||
"-" +
|
||||
(month < 10 ? "0" + month : month) +
|
||||
"-" +
|
||||
(day < 10 ? "0" + day : day) +
|
||||
" " +
|
||||
(hour < 10 ? "0" + hour : hour) +
|
||||
":" +
|
||||
(minute < 10 ? "0" + minute : minute) +
|
||||
":" +
|
||||
(second < 10 ? "0" + second : second);
|
||||
return time;
|
||||
}
|
||||
|
||||
export function SayHello() {
|
||||
return "Hello";
|
||||
}
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect, useContext } from "react";
|
||||
import { Tr, tr, langCodeContext } from "../translate";
|
||||
|
||||
function Database() {
|
||||
const [walkPath, setWalkPath] = useState("");
|
||||
const [patternString, setPatternString] = useState(
|
||||
"wav flac mp3 ogg m4a mka webm"
|
||||
);
|
||||
const [tags, setTags] = useState([]);
|
||||
const [selectedTags, setSelectedTags] = useState([]);
|
||||
const [updating, setUpdating] = useState(false);
|
||||
const { langCode } = useContext(langCodeContext);
|
||||
|
||||
function getTags() {
|
||||
fetch("/api/v1/get_tags")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
setTags(data.tags);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getTags();
|
||||
}, []);
|
||||
|
||||
function updateDatabase() {
|
||||
// split pattern string into array
|
||||
let patternArray = patternString.split(" ");
|
||||
// remove whitespace from array
|
||||
patternArray = patternArray.map((item) => item.trim());
|
||||
// remove empty strings from array
|
||||
patternArray = patternArray.filter((item) => item !== "");
|
||||
// add dot before item array
|
||||
patternArray = patternArray.map((item) => "." + item);
|
||||
|
||||
setUpdating(true);
|
||||
|
||||
fetch("/api/v1/walk", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
root: walkPath,
|
||||
pattern: patternArray,
|
||||
tag_ids: selectedTags,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
alert("Database updated");
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setUpdating(false);
|
||||
});
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h3>{Tr("Update Database")}</h3>
|
||||
<input
|
||||
type="text"
|
||||
value={walkPath}
|
||||
placeholder={tr("walk path", langCode)}
|
||||
onChange={(e) => setWalkPath(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={patternString}
|
||||
placeholder={tr("pattern wav flac mp3", langCode)}
|
||||
onChange={(e) => setPatternString(e.target.value)}
|
||||
/>
|
||||
<div>
|
||||
<h4>{Tr("Tags")}</h4>
|
||||
{tags.map((tag) => (
|
||||
<div key={tag.id}>
|
||||
<input
|
||||
id={tag.id}
|
||||
type="checkbox"
|
||||
value={tag.id}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedTags([...selectedTags, tag.id]);
|
||||
} else {
|
||||
setSelectedTags(
|
||||
selectedTags.filter((item) => item !== tag.id)
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<label htmlFor={tag.id}>{tag.name}</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
updateDatabase();
|
||||
}}
|
||||
disabled={updating}
|
||||
>
|
||||
{updating ? Tr("Updating...") : Tr("Update Database")}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Database;
|
||||
@@ -1,102 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { useParams, useNavigate } from "react-router";
|
||||
import { tr, Tr, langCodeContext } from "../translate";
|
||||
|
||||
function SingleReview() {
|
||||
let params = useParams();
|
||||
let navigate = useNavigate();
|
||||
const { langCode } = useContext(langCodeContext)
|
||||
|
||||
const [review, setReview] = useState({
|
||||
id: "",
|
||||
user_id: "",
|
||||
file_id: "",
|
||||
content: "",
|
||||
created_at: "",
|
||||
updated_at: "",
|
||||
});
|
||||
|
||||
function refresh() {
|
||||
fetch("/api/v1/get_review", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
setReview(data.review);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function save() {
|
||||
fetch("/api/v1/update_review", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
content: review.content,
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
alert(tr("Review updated", langCode));
|
||||
navigate(-1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteReview() {
|
||||
fetch("/api/v1/delete_review", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
alert(tr("Review deleted", langCode));
|
||||
navigate(-1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>{Tr("Edit Review")}</h3>
|
||||
<textarea
|
||||
value={review.content}
|
||||
onChange={(e) => setReview({ ...review, content: e.target.value })}
|
||||
></textarea>
|
||||
<div>
|
||||
<button onClick={() => deleteReview()}>{Tr("Delete")}</button>
|
||||
<button onClick={() => save()}>{Tr("Save")}</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SingleReview;
|
||||
@@ -1,143 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect, useContext } from "react";
|
||||
import { useParams, useNavigate } from "react-router";
|
||||
import { tr, Tr, langCodeContext } from "../translate";
|
||||
|
||||
function EditTag() {
|
||||
let params = useParams();
|
||||
let navigate = useNavigate();
|
||||
const { langCode } = useContext(langCodeContext);
|
||||
|
||||
const [tag, setTag] = useState({
|
||||
id: "",
|
||||
name: "",
|
||||
description: "",
|
||||
created_by_user: {
|
||||
id: "",
|
||||
username: "",
|
||||
role: "",
|
||||
avatar_id: "",
|
||||
},
|
||||
});
|
||||
|
||||
function refreshTagInfo() {
|
||||
fetch("/api/v1/get_tag_info", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
setTag(data.tag);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateTagInfo() {
|
||||
fetch("/api/v1/update_tag", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
name: tag.name,
|
||||
description: tag.description,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
alert(tr("Tag updated successfully", langCode));
|
||||
refreshTagInfo();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refreshTagInfo();
|
||||
}, []);
|
||||
|
||||
function deleteTag() {
|
||||
fetch("/api/v1/delete_tag", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
alert(tr("Tag deleted successfully", langCode));
|
||||
navigate(-1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>{Tr("Edit Tag")}</h3>
|
||||
<div>
|
||||
<label htmlFor="id">{Tr("ID")}</label>
|
||||
<input
|
||||
type="text"
|
||||
disabled
|
||||
name="id"
|
||||
id="id"
|
||||
value={tag.id}
|
||||
onChange={(e) => setTag({ ...tag, id: e.target.value })}
|
||||
/>
|
||||
<label htmlFor="name">{Tr("Created by")}</label>
|
||||
<input
|
||||
type="text"
|
||||
disabled
|
||||
name="created_by_user_username"
|
||||
id="created_by_user_username"
|
||||
value={tag.created_by_user.username}
|
||||
onChange={(e) =>
|
||||
setTag({
|
||||
...tag,
|
||||
created_by_user: {
|
||||
...tag.created_by_user,
|
||||
username: e.target.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<label htmlFor="name">{Tr("Name")}</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
value={tag.name}
|
||||
onChange={(e) => setTag({ ...tag, name: e.target.value })}
|
||||
/>
|
||||
<label htmlFor="description">{Tr("Description")}</label>
|
||||
<textarea
|
||||
name="description"
|
||||
id="description"
|
||||
value={tag.description}
|
||||
onChange={(e) => setTag({ ...tag, description: e.target.value })}
|
||||
/>
|
||||
<button onClick={deleteTag}>{Tr("Delete")}</button>
|
||||
<button onClick={() => updateTagInfo()}>{Tr("Save")}</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditTag;
|
||||
@@ -1,107 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { convertIntToDateTime } from "./Common";
|
||||
import { Tr } from "../translate";
|
||||
|
||||
function FeedbackPage() {
|
||||
const [content, setContext] = useState("");
|
||||
const [feedbacks, setFeedbacks] = useState([]);
|
||||
|
||||
function getFeedbacks() {
|
||||
fetch("/api/v1/get_feedbacks")
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
console.log(data.error);
|
||||
} else {
|
||||
setFeedbacks(data.feedbacks);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function submitFeedback() {
|
||||
fetch("/api/v1/feedback", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: content,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
setContext("");
|
||||
getFeedbacks();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getFeedbacks();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>{Tr("Feedbacks")}</h3>
|
||||
<textarea value={content} onChange={(e) => setContext(e.target.value)} />
|
||||
<button onClick={() => submitFeedback()}>{Tr("Submit")}</button>
|
||||
<div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{Tr("User")}</th>
|
||||
<th>{Tr("Feedback")}</th>
|
||||
<th>{Tr("Date")}</th>
|
||||
<th>{Tr("Action")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{feedbacks.map((feedback) => (
|
||||
<tr key={feedback._id}>
|
||||
<td>
|
||||
<Link to={`/manage/users/${feedback.user.id}`}>
|
||||
@{feedback.user.username}
|
||||
</Link>
|
||||
</td>
|
||||
<td>{feedback.content}</td>
|
||||
<td>{convertIntToDateTime(feedback.time)}</td>
|
||||
<td>
|
||||
<button
|
||||
onClick={() => {
|
||||
fetch("/api/v1/delete_feedback", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: feedback.id,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
getFeedbacks();
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{Tr("Delete")}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FeedbackPage;
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState } from "react";
|
||||
import getFfmpegConfigListRespondExample from "../example-respond/get_ffmpeg_config_list.json"
|
||||
|
||||
function FfmpegConfig(props) {
|
||||
// props.setSelectedFfmpegConfig
|
||||
@@ -8,17 +8,8 @@ function FfmpegConfig(props) {
|
||||
const [ffmpegConfigList, setFfmpegConfigList] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/v1/get_ffmpeg_config_list")
|
||||
.then((response) => response.json())
|
||||
.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);
|
||||
});
|
||||
setFfmpegConfigList(getFfmpegConfigListRespondExample.ffmpeg_config_list);
|
||||
props.setSelectedFfmpegConfig(getFfmpegConfigListRespondExample.ffmpeg_config_list[0]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -34,7 +25,7 @@ function FfmpegConfig(props) {
|
||||
<option key={ffmpegConfig.name}>{ffmpegConfig.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<span className="warp-word">{props.selectedFfmpegConfig.args}</span>
|
||||
<span>{props.selectedFfmpegConfig.args}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
43
web/src/component/FileDialog.js
Normal file
43
web/src/component/FileDialog.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
function FileDialog(props) {
|
||||
// props.showStatus
|
||||
// props.setShowStatus
|
||||
// props.playingFile
|
||||
// props.setPlayingFile
|
||||
// props.file
|
||||
|
||||
let navigate = useNavigate();
|
||||
|
||||
const downloadURL = "/api/v1/get_file_direct?id=" + props.file.id;
|
||||
|
||||
return (
|
||||
<dialog open={props.showStatus}>
|
||||
<p>{props.file.filename}</p>
|
||||
<p>
|
||||
Download using browser
|
||||
<br />
|
||||
Play on the web page
|
||||
<br />
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
props.setPlayingFile(props.file);
|
||||
props.setShowStatus(false);
|
||||
}}
|
||||
>
|
||||
Play
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate(`/file/${props.file.id}`);
|
||||
}}
|
||||
>
|
||||
Info
|
||||
</button>
|
||||
<button onClick={() => props.setShowStatus(false)}>Close</button>
|
||||
</dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default FileDialog;
|
||||
@@ -1,58 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { useNavigate } from "react-router";
|
||||
import { Tr } from "../translate";
|
||||
|
||||
function FileDialog(props) {
|
||||
// props.showStatus
|
||||
// props.setShowStatus
|
||||
// props.playingFile
|
||||
// props.setPlayingFile
|
||||
// props.file
|
||||
|
||||
let navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<dialog
|
||||
open={props.showStatus}
|
||||
style={{
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
props.setPlayingFile(props.file);
|
||||
props.setShowStatus(false);
|
||||
}}
|
||||
>
|
||||
{props.file.filename}
|
||||
</p>
|
||||
<p>
|
||||
{Tr("Play: play using browser player.")}
|
||||
<br />
|
||||
{Tr("Info for more actions.")}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate(`/files/${props.file.id}`);
|
||||
props.setShowStatus(false);
|
||||
}}
|
||||
>
|
||||
{Tr("Info")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
props.setPlayingFile(props.file);
|
||||
props.setShowStatus(false);
|
||||
}}
|
||||
>
|
||||
{Tr("Play")}
|
||||
</button>
|
||||
<button onClick={() => props.setShowStatus(false)}>{Tr("Close")}</button>
|
||||
</dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default FileDialog;
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as React from 'react';
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { CalcReadableFilesize } from "./Common";
|
||||
@@ -26,7 +25,7 @@ function FileEntry(props) {
|
||||
</td>
|
||||
<td
|
||||
className="clickable"
|
||||
onClick={() => navigate(`/folders/${props.file.folder_id}`)}
|
||||
onClick={() => navigate(`/folder/${props.file.folder_id}`)}
|
||||
>
|
||||
{props.file.foldername}
|
||||
</td>
|
||||
69
web/src/component/FileInfo.js
Normal file
69
web/src/component/FileInfo.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import { useParams, Link, useNavigate } from "react-router-dom";
|
||||
|
||||
function FileInfo() {
|
||||
let params = useParams();
|
||||
let navigate = useNavigate();
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>File Information</h3>
|
||||
<span>
|
||||
<button>Download</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate("/file/" + params.id + '/share');
|
||||
}}
|
||||
>
|
||||
Share
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate("/file/" + params.id + '/review');
|
||||
}}
|
||||
>Review</button>
|
||||
</span>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>File Name</td>
|
||||
<td>{params.id}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>File Size</td>
|
||||
<td>123456</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>File Type</td>
|
||||
<td>media/aac</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Last Modified</td>
|
||||
<td>2020-01-01</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Import by</td>
|
||||
<td>
|
||||
<Link to="/profile/3">@admin</Link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Import Date</td>
|
||||
<td>2020-01-01</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Location</td>
|
||||
<td>/data/media/aac</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<button>Update</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FileInfo;
|
||||
@@ -1,331 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import {useNavigate, useParams} from "react-router";
|
||||
import {useContext, useEffect, useState} from "react";
|
||||
import {Tr, tr, langCodeContext} from "../translate";
|
||||
|
||||
function FileInfo(props) {
|
||||
let navigate = useNavigate();
|
||||
let params = useParams();
|
||||
const [file, setFile] = useState({
|
||||
id: "",
|
||||
folder_id: "",
|
||||
foldername: "",
|
||||
filename: "",
|
||||
filesize: "",
|
||||
});
|
||||
const [tags, setTags] = useState([]);
|
||||
const [tagsOnFile, setTagsOnFile] = useState([]);
|
||||
const [selectedTagID, setSelectedTagID] = useState("");
|
||||
const {langCode} = useContext(langCodeContext);
|
||||
const [ffprobeInfo, setFfprobeInfo] = useState("");
|
||||
|
||||
function refresh() {
|
||||
fetch(`/api/v1/get_file_info`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
setFile(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getTags() {
|
||||
fetch(`/api/v1/get_tags`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
setTags(data.tags);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getTagsOnFile() {
|
||||
fetch(`/api/v1/get_tags_on_file`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
setTagsOnFile(data.tags);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removeTagOnFile(tag_id) {
|
||||
fetch(`/api/v1/delete_tag_on_file`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
file_id: parseInt(params.id),
|
||||
tag_id: tag_id,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
getTagsOnFile();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteFile() {
|
||||
// show Warning
|
||||
if (
|
||||
window.confirm(tr("Are you sure you want to delete this file?", langCode))
|
||||
) {
|
||||
fetch(`/api/v1/delete_file`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
navigate(-1);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateFilename() {
|
||||
fetch(`/api/v1/update_filename`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
filename: file.filename,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
alert(tr("Filename updated", langCode));
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function resetFilename() {
|
||||
fetch(`/api/v1/reset_filename`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
getTags();
|
||||
getTagsOnFile();
|
||||
}, []);
|
||||
|
||||
const downloadURL = "/api/v1/get_file_direct?id=" + file.id;
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>{Tr("File Details")}</h3>
|
||||
<div>
|
||||
<a href={downloadURL} download>
|
||||
<button>{Tr("Download")}</button>
|
||||
</a>
|
||||
<button
|
||||
onClick={() => {
|
||||
props.setPlayingFile(file);
|
||||
}}
|
||||
>
|
||||
{Tr("Play")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate(`/files/${params.id}/review`);
|
||||
}}
|
||||
>
|
||||
{Tr("Review")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate(`/files/${params.id}/share`);
|
||||
}}
|
||||
>
|
||||
{Tr("Share")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
deleteFile();
|
||||
}}
|
||||
>
|
||||
{Tr("Delete")}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="foldername">{Tr("Folder Name")}</label>
|
||||
<input
|
||||
type="text"
|
||||
id="foldername"
|
||||
value={file.foldername}
|
||||
onClick={() => {
|
||||
navigate(`/folders/${file.folder_id}`);
|
||||
}}
|
||||
readOnly
|
||||
/>
|
||||
<label htmlFor="filename">{Tr("Filename")}</label>
|
||||
<input
|
||||
type="text"
|
||||
id="filename"
|
||||
value={file.filename}
|
||||
onChange={(event) => {
|
||||
setFile({
|
||||
...file,
|
||||
filename: event.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<label htmlFor="filesize">{Tr("File size")}</label>
|
||||
<input type="text" id="filesize" value={file.filesize} readOnly />
|
||||
</div>
|
||||
<div className="horizontal">
|
||||
<button onClick={updateFilename}>{Tr("Save")}</button>
|
||||
<button onClick={resetFilename}>{Tr("Reset")}</button>
|
||||
</div>
|
||||
<div>
|
||||
<label>{Tr("Tags")}</label>
|
||||
<ul>
|
||||
{tagsOnFile.map((tag) => {
|
||||
return (
|
||||
<li key={tag.id}>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate(`/manage/tags/${tag.id}`);
|
||||
}}
|
||||
>
|
||||
{tag.name}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
removeTagOnFile(tag.id);
|
||||
}}
|
||||
>
|
||||
{Tr("Remove")}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<div>
|
||||
<select
|
||||
onChange={(e) => {
|
||||
setSelectedTagID(e.target.value);
|
||||
}}
|
||||
>
|
||||
<option value="">{tr("Select a tag", langCode)}</option>
|
||||
{tags.map((tag) => {
|
||||
return (
|
||||
<option key={tag.id} value={tag.id}>
|
||||
{tag.name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
<button
|
||||
onClick={() => {
|
||||
// check empty
|
||||
if (selectedTagID === "") {
|
||||
alert(tr("Please select a tag", langCode));
|
||||
return;
|
||||
}
|
||||
fetch(`/api/v1/put_tag_on_file`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
file_id: parseInt(params.id),
|
||||
tag_id: parseInt(selectedTagID),
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
getTagsOnFile();
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{Tr("Add tag")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button onClick={async () => {
|
||||
const resp = await fetch(`/api/v1/get_file_ffprobe_info`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
}),
|
||||
});
|
||||
const text = await resp.text();
|
||||
setFfprobeInfo(text);
|
||||
}}>FFprobe</button>
|
||||
|
||||
{ffprobeInfo && <textarea
|
||||
style={{
|
||||
height: "30em",
|
||||
}}
|
||||
>{ffprobeInfo}</textarea>}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FileInfo;
|
||||
15
web/src/component/FilesInFolder.js
Normal file
15
web/src/component/FilesInFolder.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import FilesTable from "./FilesTable";
|
||||
import searchFilesRespondExample from "../example-respond/search_files.json"
|
||||
import {useParams} from "react-router";
|
||||
|
||||
function FilesInFolder(props) {
|
||||
let params = useParams();
|
||||
return (
|
||||
<div>
|
||||
<h3>Files in folder id {params.id}</h3>
|
||||
<FilesTable setPlayingFile={props.setPlayingFile} files={searchFilesRespondExample.files} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FilesInFolder;
|
||||
@@ -1,141 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import {useParams} from "react-router";
|
||||
import {useState, useEffect} from "react";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {useQuery} from "./Common";
|
||||
import FilesTable from "./FilesTable";
|
||||
import {Tr} from "../translate";
|
||||
|
||||
function FilesInFolder(props) {
|
||||
let params = useParams();
|
||||
const query = useQuery();
|
||||
const navigator = useNavigate();
|
||||
const [files, setFiles] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const offset = parseInt(query.get("o")) || 0;
|
||||
const [newFoldername, setNewFoldername] = useState("");
|
||||
const [folderPath, setFolderPath] = useState("");
|
||||
const limit = 10;
|
||||
|
||||
function refresh() {
|
||||
setIsLoading(true);
|
||||
fetch("/api/v1/get_files_in_folder", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({
|
||||
folder_id: parseInt(params.id),
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
setFiles(data.files);
|
||||
setFolderPath(data.folder);
|
||||
if (data.files.length > 0) {
|
||||
setNewFoldername(data.files[0].foldername);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => alert(error))
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [params.id, offset]);
|
||||
|
||||
function nextPage() {
|
||||
navigator(`/folders/${params.id}?o=${offset + limit}`);
|
||||
}
|
||||
|
||||
function lastPage() {
|
||||
const offsetValue = offset - limit;
|
||||
if (offsetValue < 0) {
|
||||
return;
|
||||
}
|
||||
navigator(`/folders/${params.id}?o=${offsetValue}`);
|
||||
}
|
||||
|
||||
function updateFoldername() {
|
||||
setIsLoading(true);
|
||||
fetch("/api/v1/update_foldername", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
foldername: newFoldername,
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
refresh();
|
||||
}
|
||||
})
|
||||
.catch((error) => alert(error))
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
function resetFoldername() {
|
||||
setIsLoading(true);
|
||||
fetch("/api/v1/reset_foldername", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
refresh();
|
||||
}
|
||||
})
|
||||
.catch((error) => alert(error))
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>{Tr("Files in Folder")}</h3>
|
||||
<div className="search_toolbar">
|
||||
<button onClick={lastPage}>{Tr("Last page")}</button>
|
||||
<button disabled>
|
||||
{isLoading
|
||||
? Tr("Loading...")
|
||||
: `${offset} - ${offset + files.length}`}
|
||||
</button>
|
||||
<button onClick={nextPage}>{Tr("Next page")}</button>
|
||||
</div>
|
||||
<FilesTable setPlayingFile={props.setPlayingFile} files={files} />
|
||||
<span>{folderPath}</span>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={newFoldername}
|
||||
onChange={(e) => setNewFoldername(e.target.value)}
|
||||
/>
|
||||
<div>
|
||||
<button onClick={() => updateFoldername()}>{Tr("Save")}</button>
|
||||
<button onClick={() => resetFoldername()}>{Tr("Reset")}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FilesInFolder;
|
||||
@@ -1,18 +1,13 @@
|
||||
import * as React from 'react';
|
||||
import FileEntry from "./FileEntry";
|
||||
import { Tr } from "../translate";
|
||||
|
||||
function FilesTable(props) {
|
||||
if (props.files.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{Tr("Filename")}</th>
|
||||
<th>{Tr("Folder Name")}</th>
|
||||
<th>{Tr("Size")}</th>
|
||||
<th>Filename</th>
|
||||
<th>Folder Name</th>
|
||||
<th>Size</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -1,31 +1,26 @@
|
||||
import * as React from 'react';
|
||||
import { useNavigate } from "react-router";
|
||||
import { Tr } from "../translate";
|
||||
|
||||
function FoldersTable(props) {
|
||||
let navigate = useNavigate();
|
||||
if (props.folders.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{Tr("Folder name")}</th>
|
||||
<th>{Tr("Action")}</th>
|
||||
<th>Folder name</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{props.folders.map((folder) => (
|
||||
<tr key={folder.id}>
|
||||
<td
|
||||
onClick={() => navigate(`/folders/${folder.id}`)}
|
||||
onClick={() => navigate(`/folder/${folder.id}`)}
|
||||
className="clickable"
|
||||
>
|
||||
{folder.foldername}
|
||||
</td>
|
||||
<td onClick={() => navigate(`/folders/${folder.id}`)}>
|
||||
<button>{Tr("View")}</button>
|
||||
<td onClick={() => navigate(`/folder/${folder.id}`)}>
|
||||
<button>View</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
28
web/src/component/GetRandomFiles.js
Normal file
28
web/src/component/GetRandomFiles.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import FilesTable from "./FilesTable";
|
||||
import getRandomFilesRespondExample from "../example-respond/get_random_files.json"
|
||||
|
||||
function GetRandomFiles(props) {
|
||||
const [files, setFiles] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
function refresh(setFiles) {
|
||||
setFiles(getRandomFilesRespondExample.files);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refresh(setFiles);
|
||||
}, []);
|
||||
return (
|
||||
<div className="page">
|
||||
<div className="search_toolbar">
|
||||
<button className="refresh" onClick={() => refresh(setFiles)}>
|
||||
{isLoading ? "Loading..." : "Refresh"}
|
||||
</button>
|
||||
</div>
|
||||
<FilesTable setPlayingFile={props.setPlayingFile} files={files} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GetRandomFiles;
|
||||
@@ -1,118 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useQuery } from "./Common";
|
||||
import FilesTable from "./FilesTable";
|
||||
import { Tr, tr, langCodeContext } from "../translate";
|
||||
|
||||
function GetRandomFiles(props) {
|
||||
const [files, setFiles] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [tags, setTags] = useState([]);
|
||||
const navigator = useNavigate();
|
||||
const query = useQuery();
|
||||
const selectedTag = query.get("t") || "";
|
||||
const { langCode } = useContext(langCodeContext);
|
||||
|
||||
const fetchRandomFiles = async () => {
|
||||
const resp = await fetch("/api/v1/get_random_files");
|
||||
const json = await resp.json();
|
||||
return json.files;
|
||||
};
|
||||
|
||||
async function getRandomFiles() {
|
||||
setIsLoading(true);
|
||||
fetchRandomFiles()
|
||||
.then((data) => {
|
||||
setFiles(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
alert("get_random_files error: " + error);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
const fetchRandomFilesWithTag = async (selectedTag) => {
|
||||
const resp = await fetch("/api/v1/get_random_files_with_tag", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(selectedTag),
|
||||
}),
|
||||
});
|
||||
const json = await resp.json();
|
||||
return json.files;
|
||||
};
|
||||
|
||||
function getRandomFilesWithTag() {
|
||||
setIsLoading(true);
|
||||
fetchRandomFilesWithTag(selectedTag)
|
||||
.then((files) => {
|
||||
setFiles(files);
|
||||
})
|
||||
.catch((error) => {
|
||||
alert("get_random_files_with_tag error: " + error);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
if (selectedTag === "") {
|
||||
getRandomFiles();
|
||||
} else {
|
||||
getRandomFilesWithTag();
|
||||
}
|
||||
}
|
||||
|
||||
function getTags() {
|
||||
fetch("/api/v1/get_tags")
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
setTags(data.tags);
|
||||
})
|
||||
.catch((error) => {
|
||||
alert("get_tags error: " + error);
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getTags();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [selectedTag]);
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<div className="search_toolbar">
|
||||
<button className="refresh" onClick={() => refresh(setFiles)}>
|
||||
{isLoading ? Tr("Loading...") : Tr("Refresh")}
|
||||
</button>
|
||||
<select
|
||||
className="tag_select"
|
||||
onChange={(event) => {
|
||||
navigator(`/?t=${event.target.value}`);
|
||||
}}
|
||||
value={selectedTag}
|
||||
>
|
||||
<option value="">{tr("All", langCode)}</option>
|
||||
{tags.map((tag) => (
|
||||
<option key={tag.id} value={tag.id}>
|
||||
{tag.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<FilesTable setPlayingFile={props.setPlayingFile} files={files} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GetRandomFiles;
|
||||
48
web/src/component/Login.js
Normal file
48
web/src/component/Login.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
|
||||
function Login(props) {
|
||||
let navigate = useNavigate();
|
||||
let [username, setUsername] = useState("");
|
||||
let [password, setPassword] = useState("");
|
||||
return (
|
||||
<div>
|
||||
<h1>Login</h1>
|
||||
<label htmlFor="username"></label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
<label htmlFor="password">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<span>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!username || !password) {
|
||||
alert("Please enter username and password");
|
||||
return;
|
||||
}
|
||||
props.setUser({ id: 123, username: username, password: password });
|
||||
navigate("/");
|
||||
}}
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate("/register");
|
||||
}}
|
||||
>Register</button>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Login;
|
||||
@@ -1,75 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useContext, useState } from "react";
|
||||
import { Tr, tr, langCodeContext } from "../translate";
|
||||
|
||||
function Login(props) {
|
||||
let navigate = useNavigate();
|
||||
let [username, setUsername] = useState("");
|
||||
let [password, setPassword] = useState("");
|
||||
const { langCode } = useContext(langCodeContext);
|
||||
|
||||
function login() {
|
||||
if (!username || !password) {
|
||||
alert(tr("Please enter username and password", langCode));
|
||||
return;
|
||||
}
|
||||
fetch("/api/v1/login", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
password,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
return;
|
||||
}
|
||||
props.setUser(data.user);
|
||||
navigate("/");
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h2>{Tr("Login")}</h2>
|
||||
<label htmlFor="username">{Tr("Username")}</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
<label htmlFor="password">{Tr("Password")}</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
login();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span>
|
||||
<button onClick={login}>{Tr("Login")}</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate("/manage/register");
|
||||
}}
|
||||
>
|
||||
{Tr("Register")}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Login;
|
||||
104
web/src/component/Manage.js
Normal file
104
web/src/component/Manage.js
Normal file
@@ -0,0 +1,104 @@
|
||||
import getFfmpegConfigListRespondExample from "../example-respond/get_ffmpeg_config_list.json";
|
||||
|
||||
function Manage() {
|
||||
return (
|
||||
<div className="page">
|
||||
<h2>Manage</h2>
|
||||
<h3>Server status</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Server status</td>
|
||||
<td>
|
||||
<span className="status-ok">OK</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Server uptime</td>
|
||||
<td>
|
||||
<span>1 day, 23 hours, 59 minutes and 59 seconds</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Server load</td>
|
||||
<td>
|
||||
<span>0.00 / 0.00 / 0.00</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Server memory usage</td>
|
||||
<td>
|
||||
<span>0.00 MB</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Server disk usage</td>
|
||||
<td>
|
||||
<span>0.00 MB</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Server uptime</td>
|
||||
<td>
|
||||
<span>1 day, 23 hours, 59 minutes and 59 seconds</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Server load</td>
|
||||
<td>
|
||||
<span>0.00 / 0.00 / 0.00</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Server memory usage</td>
|
||||
<td>
|
||||
<span>0.00 MB</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Server disk usage</td>
|
||||
<td>
|
||||
<span>0.00 MB</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>Database opeartions</h3>
|
||||
<ul>
|
||||
<li>.mp3</li>
|
||||
<li>.flac</li>
|
||||
<li>.wav</li>
|
||||
<li>.ogg</li>
|
||||
<li>.aac</li>
|
||||
<li>.m4a</li>
|
||||
</ul>
|
||||
<input type="text" placeholder=".mp3" />
|
||||
<button>Add Pattern</button>
|
||||
<input type="text" placeholder="/path/to/root" />
|
||||
<button>Import</button>
|
||||
<h3>Ffmpeg Settings</h3>
|
||||
<ol>
|
||||
{getFfmpegConfigListRespondExample.ffmpeg_config_list.map(
|
||||
(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>
|
||||
);
|
||||
}
|
||||
|
||||
export default Manage;
|
||||
@@ -1,101 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { useNavigate } from "react-router";
|
||||
import Database from "./Database";
|
||||
|
||||
import { Tr, langCodeContext, LANG_OPTIONS } from "../translate";
|
||||
import { useContext } from "react";
|
||||
|
||||
function Manage(props) {
|
||||
let navigate = useNavigate();
|
||||
const { langCode, setLangCode } = useContext(langCodeContext);
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h2>{Tr("Manage")}</h2>
|
||||
<p>
|
||||
{Tr("Hi")}, {props.user.username}
|
||||
</p>
|
||||
|
||||
<select
|
||||
value={langCode}
|
||||
onChange={(event) => {
|
||||
setLangCode(event.target.value);
|
||||
}}
|
||||
>
|
||||
{Object.keys(LANG_OPTIONS).map((code) => {
|
||||
const langOption = LANG_OPTIONS[code];
|
||||
return (
|
||||
<option value={code} key={code}>
|
||||
{langOption.name}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
|
||||
{props.user.role === 0 && (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate("/manage/login");
|
||||
}}
|
||||
>
|
||||
{Tr("Login")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate("/manage/register");
|
||||
}}
|
||||
>
|
||||
{Tr("Register")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{props.user.role !== 0 && (
|
||||
<div className="horizontal">
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate(`/manage/users/${props.user.id}`);
|
||||
}}
|
||||
>
|
||||
{Tr("Profile")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
fetch("/api/v1/logout")
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
props.setUser(data.user);
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{Tr("Logout")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<hr />
|
||||
<div className="horizontal">
|
||||
<button onClick={() => navigate("/manage/tags")}>{Tr("Tags")}</button>
|
||||
<button onClick={() => navigate("/manage/users")}>{Tr("Users")}</button>
|
||||
<button onClick={() => navigate("/manage/feedbacks")}>
|
||||
{Tr("Feedbacks")}
|
||||
</button>
|
||||
</div>
|
||||
<Database />
|
||||
<p>
|
||||
<a
|
||||
href="https://github.com/heimoshuiyu/msw-open-music"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{Tr("View source code on Github")}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Manage;
|
||||
@@ -1,82 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Tr } from "../translate";
|
||||
|
||||
function ManageUser() {
|
||||
const [users, setUsers] = useState([]);
|
||||
const roleDict = {
|
||||
0: "Anonymous",
|
||||
1: "Admin",
|
||||
2: "User",
|
||||
};
|
||||
|
||||
function getUsers() {
|
||||
fetch("/api/v1/get_users")
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
setUsers(data.users);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getUsers();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>{Tr("Manage User")}</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{Tr("Name")}</th>
|
||||
<th>{Tr("Role")}</th>
|
||||
<th>{Tr("Active")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map((user) => (
|
||||
<tr key={user.id}>
|
||||
<td>
|
||||
<Link to={`/manage/users/${user.id}`}>@{user.username}</Link>
|
||||
</td>
|
||||
<td>{Tr(roleDict[user.role])}</td>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
defaultChecked={user.active}
|
||||
onClick={(e) => {
|
||||
fetch("/api/v1/update_user_active", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: user.id,
|
||||
active: e.target.checked,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
getUsers();
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ManageUser;
|
||||
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;
|
||||
51
web/src/component/Register.js
Normal file
51
web/src/component/Register.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
|
||||
function Register(props) {
|
||||
let navigate = useNavigate();
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [password2, setPassword2] = useState("");
|
||||
return (
|
||||
<div>
|
||||
<h1>Register</h1>
|
||||
<label htmlFor="username">Username:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
<label htmlFor="password">Password:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<label htmlFor="password2">Confirm Password:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password2"
|
||||
value={password2}
|
||||
onChange={(e) => setPassword2(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
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("/");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Register
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Register;
|
||||
@@ -1,83 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useContext, useState } from "react";
|
||||
import { tr, Tr, langCodeContext } from "../translate";
|
||||
|
||||
function Register() {
|
||||
let navigate = useNavigate();
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [password2, setPassword2] = useState("");
|
||||
const [role, setRole] = useState("");
|
||||
const { langCode } = useContext(langCodeContext);
|
||||
|
||||
function register() {
|
||||
if (!username || !password || !password2 || !role) {
|
||||
alert(tr("Please fill out all fields", langCode));
|
||||
} else if (password !== password2) {
|
||||
alert(tr("Password do not match", langCode));
|
||||
} else {
|
||||
fetch("/api/v1/register", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: username,
|
||||
password: password,
|
||||
role: parseInt(role),
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
navigate("/manage/login");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h2>{Tr("Register")}</h2>
|
||||
<label htmlFor="username">{Tr("Username")}</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
<label htmlFor="password">{Tr("Password")}</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<label htmlFor="password2">{Tr("Confirm Password")}</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password2"
|
||||
value={password2}
|
||||
onChange={(e) => setPassword2(e.target.value)}
|
||||
onKeyPress={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
register();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<label htmlFor="role">{Tr("Role")}</label>
|
||||
<select value={role} onChange={(e) => setRole(e.target.value)}>
|
||||
<option value="">{tr("Select a role", langCode)}</option>
|
||||
<option value="2">{tr("User", langCode)}</option>
|
||||
<option value="1">{tr("Admin", langCode)}</option>
|
||||
</select>
|
||||
<button onClick={register}>{Tr("Register")}</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Register;
|
||||
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;
|
||||
18
web/src/component/ReviewEntry.js
Normal file
18
web/src/component/ReviewEntry.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Link} from "react-router-dom";
|
||||
|
||||
function ReviewEntry() {
|
||||
return (
|
||||
<p>
|
||||
<h5>
|
||||
<Link to="/profile/2">@rin</Link> comment music ID 39 at
|
||||
2019-01-01 12:23:45
|
||||
</h5>
|
||||
Agree with <Link to="/profile/1">@hmsy</Link>. I also like how well the
|
||||
musician plays the guitar. They are all very good. They really make the
|
||||
song sound better. I like the way the bass plays and the way the guitar
|
||||
sounds. I like the way the drums sound.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReviewEntry;
|
||||
@@ -1,34 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { Link } from "react-router-dom";
|
||||
import { convertIntToDateTime } from "./Common";
|
||||
import { Tr, tr, langCodeContext } from "../translate";
|
||||
import { useContext } from "react";
|
||||
|
||||
function ReviewEntry(props) {
|
||||
const { langCode } = useContext(langCodeContext);
|
||||
return (
|
||||
<div>
|
||||
<h4>
|
||||
<Link to={`/manage/users/${props.review.user.id}`}>
|
||||
@{props.review.user.username}
|
||||
</Link>{" "}
|
||||
{Tr("review")}{" "}
|
||||
<Link to={`/files/${props.review.file.id}`}>
|
||||
{props.review.file.filename}
|
||||
</Link>{" "}
|
||||
{Tr("on")} {convertIntToDateTime(props.review.created_at)}{" "}
|
||||
{props.review.updated_at !== 0 &&
|
||||
`(${tr("modified on", langCode)} ${convertIntToDateTime(
|
||||
props.review.updated_at
|
||||
)} ) `}
|
||||
{(props.user.role === 1 || props.review.user.id === props.user.id) &&
|
||||
props.user.role !== 0 && (
|
||||
<Link to={`/manage/reviews/${props.review.id}`}>{Tr("Edit")}</Link>
|
||||
)}
|
||||
</h4>
|
||||
<p>{props.review.content}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReviewEntry;
|
||||
@@ -1,77 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect } from "react";
|
||||
import { useParams } from "react-router";
|
||||
import ReviewEntry from "./ReviewEntry";
|
||||
import { Tr } from "../translate";
|
||||
|
||||
function ReviewPage(props) {
|
||||
let params = useParams();
|
||||
const [newReview, setNewReview] = useState("");
|
||||
const [reviews, setReviews] = useState([]);
|
||||
|
||||
function refresh() {
|
||||
fetch("/api/v1/get_reviews_on_file", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
setReviews(data.reviews);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, []);
|
||||
|
||||
function submitReview() {
|
||||
fetch("/api/v1/insert_review", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: newReview,
|
||||
file_id: parseInt(params.id),
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
setNewReview("");
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>{Tr("Review Page")}</h3>
|
||||
<div>
|
||||
{reviews.map((review) => (
|
||||
<ReviewEntry key={review.id} review={review} user={props.user} />
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<textarea
|
||||
value={newReview}
|
||||
onChange={(e) => setNewReview(e.target.value)}
|
||||
/>
|
||||
<button onClick={() => submitReview()}>{Tr("Submit")}</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReviewPage;
|
||||
68
web/src/component/SearchFiles.js
Normal file
68
web/src/component/SearchFiles.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import FilesTable from "./FilesTable";
|
||||
import searchFilesRespondExample from "../example-respond/search_files.json"
|
||||
|
||||
function SearchFiles(props) {
|
||||
const [files, setFiles] = useState([]);
|
||||
const [filename, setFilename] = useState("");
|
||||
const [offset, setOffset] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const limit = 10;
|
||||
|
||||
function searchFiles() {
|
||||
setFiles(searchFilesRespondExample.files);
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
setOffset(offset + limit);
|
||||
}
|
||||
|
||||
function lastPage() {
|
||||
const offsetValue = offset - limit;
|
||||
if (offsetValue < 0) {
|
||||
return;
|
||||
}
|
||||
setOffset(offsetValue);
|
||||
}
|
||||
|
||||
useEffect(() => searchFiles(), [offset, props.folder]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>Search Files</h3>
|
||||
<div className="search_toolbar">
|
||||
{!props.folder && (
|
||||
<input
|
||||
onChange={(event) => setFilename(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
searchFiles();
|
||||
}
|
||||
}}
|
||||
type="text"
|
||||
placeholder="Enter filename"
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
disabled={!!props.folder}
|
||||
onClick={() => {
|
||||
searchFiles();
|
||||
}}
|
||||
>
|
||||
{isLoading ? "Loading..." : "Search"}
|
||||
</button>
|
||||
{props.folder && props.folder.foldername && (
|
||||
<button onClick={searchFiles}>{props.folder.foldername}</button>
|
||||
)}
|
||||
<button onClick={lastPage}>Last page</button>
|
||||
<button disabled>
|
||||
{offset} - {offset + files.length}
|
||||
</button>
|
||||
<button onClick={nextPage}>Next page</button>
|
||||
</div>
|
||||
<FilesTable setPlayingFile={props.setPlayingFile} files={files} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchFiles;
|
||||
@@ -1,94 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect, useContext } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useQuery } from "./Common";
|
||||
import FilesTable from "./FilesTable";
|
||||
import { Tr, tr, langCodeContext } from "../translate";
|
||||
|
||||
function SearchFiles(props) {
|
||||
const navigator = useNavigate();
|
||||
const [files, setFiles] = useState([]);
|
||||
const query = useQuery();
|
||||
const filename = query.get("q") || "";
|
||||
const [filenameInput, setFilenameInput] = useState(filename);
|
||||
const offset = parseInt(query.get("o")) || 0;
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const limit = 10;
|
||||
const { langCode } = useContext(langCodeContext);
|
||||
|
||||
function searchFiles() {
|
||||
// check empty filename
|
||||
if (filename === "") {
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
fetch("/api/v1/search_files", {
|
||||
method: "POST",
|
||||
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() {
|
||||
navigator(`/files?q=${filenameInput}&o=${offset + limit}`);
|
||||
}
|
||||
|
||||
function lastPage() {
|
||||
const offsetValue = offset - limit;
|
||||
if (offsetValue < 0) {
|
||||
return;
|
||||
}
|
||||
navigator(`/files?q=${filenameInput}&o=${offsetValue}`);
|
||||
}
|
||||
|
||||
useEffect(() => searchFiles(), [offset, filename]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>{Tr("Search Files")}</h3>
|
||||
<div className="search_toolbar">
|
||||
<input
|
||||
onChange={(event) => setFilenameInput(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
navigator(`/files?q=${filenameInput}&o=0`);
|
||||
}
|
||||
}}
|
||||
type="text"
|
||||
placeholder={tr("Enter filename", langCode)}
|
||||
value={filenameInput}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator(`/files?q=${filenameInput}&o=0`);
|
||||
}}
|
||||
>
|
||||
{isLoading ? Tr("Loading...") : Tr("Search")}
|
||||
</button>
|
||||
<button onClick={lastPage}>{Tr("Last page")}</button>
|
||||
<button disabled>
|
||||
{offset} - {offset + files.length}
|
||||
</button>
|
||||
<button onClick={nextPage}>{Tr("Next page")}</button>
|
||||
</div>
|
||||
<FilesTable setPlayingFile={props.setPlayingFile} files={files} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchFiles;
|
||||
72
web/src/component/SearchFolders.js
Normal file
72
web/src/component/SearchFolders.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router";
|
||||
import FoldersTable from "./FoldersTable";
|
||||
import SearchFiles from "./SearchFiles";
|
||||
import searchFoldersRespondExample from "../example-respond/search_folders.json";
|
||||
|
||||
function SearchFolders(props) {
|
||||
const [foldername, setFoldername] = useState("");
|
||||
const [folders, setFolders] = useState([]);
|
||||
const [folder, setFolder] = useState({});
|
||||
const [offset, setOffset] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const limit = 10;
|
||||
|
||||
function searchFolder() {
|
||||
setFolders(searchFoldersRespondExample.folders);
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
setOffset(offset + limit);
|
||||
}
|
||||
|
||||
function lastPage() {
|
||||
const offsetValue = offset - limit;
|
||||
if (offsetValue < 0) {
|
||||
return;
|
||||
}
|
||||
setOffset(offsetValue);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="page">
|
||||
<h3>Search Folders</h3>
|
||||
<div className="search_toolbar">
|
||||
<input
|
||||
onChange={(event) => setFoldername(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
searchFolder();
|
||||
}
|
||||
}}
|
||||
type="text"
|
||||
placeholder="Enter folder name"
|
||||
/>
|
||||
<button onClick={searchFolder}>
|
||||
{isLoading ? "Loading..." : "Search"}
|
||||
</button>
|
||||
<button onClick={lastPage}>Last page</button>
|
||||
<button disabled>
|
||||
{offset} - {offset + limit}
|
||||
</button>
|
||||
<button onClick={nextPage}>Next page</button>
|
||||
</div>
|
||||
<FoldersTable viewFolder={viewFolder} folders={folders} />
|
||||
<SearchFiles setPlayingFile={props.setPlayingFile} folder={folder} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchFolders;
|
||||
@@ -1,92 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useQuery } from "./Common";
|
||||
import FoldersTable from "./FoldersTable";
|
||||
import { Tr, tr, langCodeContext } from "../translate";
|
||||
|
||||
function SearchFolders() {
|
||||
const navigator = useNavigate();
|
||||
const query = useQuery();
|
||||
const foldername = query.get("q") || "";
|
||||
const [foldernameInput, setFoldernameInput] = useState(foldername);
|
||||
const [folders, setFolders] = useState([]);
|
||||
const offset = parseInt(query.get("o")) || 0;
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const limit = 10;
|
||||
const { langCode } = useContext(langCodeContext);
|
||||
|
||||
function searchFolder() {
|
||||
if (foldername === "") {
|
||||
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() {
|
||||
navigator(`/folders?q=${foldername}&o=${offset + limit}`);
|
||||
}
|
||||
|
||||
function lastPage() {
|
||||
const offsetValue = offset - limit;
|
||||
if (offsetValue < 0) {
|
||||
return;
|
||||
}
|
||||
navigator(`/folders?q=${foldername}&o=${offsetValue}`);
|
||||
}
|
||||
|
||||
useEffect(() => searchFolder(), [offset, foldername]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>{Tr("Search Folders")}</h3>
|
||||
<div className="search_toolbar">
|
||||
<input
|
||||
onChange={(event) => setFoldernameInput(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
navigator(`/folders?q=${foldernameInput}&o=0`);
|
||||
}
|
||||
}}
|
||||
type="text"
|
||||
placeholder={tr("Enter folder name", langCode)}
|
||||
value={foldernameInput}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator(`/folders?q=${foldernameInput}&o=0`);
|
||||
}}
|
||||
>
|
||||
{isLoading ? Tr("Loading...") : Tr("Search")}
|
||||
</button>
|
||||
<button onClick={lastPage}>{Tr("Last page")}</button>
|
||||
<button disabled>
|
||||
{offset} - {offset + limit}
|
||||
</button>
|
||||
<button onClick={nextPage}>{Tr("Next page")}</button>
|
||||
</div>
|
||||
<FoldersTable folders={folders} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchFolders;
|
||||
27
web/src/component/Share.js
Normal file
27
web/src/component/Share.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router";
|
||||
import FilesTable from "./FilesTable";
|
||||
import GetFileInfoRespondExample from "../example-respond/get_file_info.json";
|
||||
|
||||
function Share(props) {
|
||||
let params = useParams();
|
||||
const [file, setFile] = useState([]);
|
||||
useEffect(() => {
|
||||
setFile([GetFileInfoRespondExample]);
|
||||
}, [params]);
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>Share with others!</h3>
|
||||
<p>
|
||||
👇 Click the filename below to enjoy music!
|
||||
<br />
|
||||
</p>
|
||||
<p>
|
||||
Share link: <a href={window.location.href}>{window.location.href}</a>
|
||||
</p>
|
||||
<FilesTable setPlayingFile={props.setPlayingFile} files={file} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Share;
|
||||
@@ -1,59 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { useParams } from "react-router";
|
||||
import FilesTable from "./FilesTable";
|
||||
import { Tr, tr, langCodeContext } from "../translate";
|
||||
|
||||
function Share(props) {
|
||||
let params = useParams();
|
||||
const { langCode } = useContext(langCodeContext);
|
||||
|
||||
const [file, setFile] = useState({});
|
||||
useEffect(() => {
|
||||
fetch("/api/v1/get_file_info", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
id: parseInt(params.id),
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
setFile(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
alert("get_file_info error: " + error);
|
||||
});
|
||||
}, [params]);
|
||||
|
||||
// change title
|
||||
useEffect(() => {
|
||||
const oldTitle = document.title;
|
||||
|
||||
document.title = `${tr("Share", langCode)}🎵: ${
|
||||
file.filename
|
||||
} - MSW Open Music`;
|
||||
|
||||
// set title back
|
||||
return () => {
|
||||
document.title = oldTitle;
|
||||
};
|
||||
}, [file]);
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>{Tr("Share with others!")}</h3>
|
||||
<p>
|
||||
{Tr("Share link")}:{" "}
|
||||
<a href={window.location.href}>{window.location.href}</a>
|
||||
</p>
|
||||
<p>
|
||||
👇 {Tr("Click the filename below to enjoy music!")}
|
||||
<br />
|
||||
</p>
|
||||
<FilesTable setPlayingFile={props.setPlayingFile} files={[file]} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Share;
|
||||
@@ -1,107 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Tr } from "../translate";
|
||||
|
||||
function Tags() {
|
||||
const [tags, setTags] = useState([]);
|
||||
const [newTagName, setNewTagName] = useState("");
|
||||
const [newTagDescription, setNewTagDescription] = useState("");
|
||||
const [showAddTag, setShowAddTag] = useState(false);
|
||||
|
||||
function refresh() {
|
||||
fetch("/api/v1/get_tags")
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
setTags(data.tags);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h3>{Tr("Tags")}</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{Tr("Name")}</th>
|
||||
<th>{Tr("Description")}</th>
|
||||
<th>{Tr("Created by")}</th>
|
||||
<th>{Tr("Action")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tags.map((tag) => (
|
||||
<tr key={tag.id}>
|
||||
<td>{tag.name}</td>
|
||||
<td>{tag.description}</td>
|
||||
<td>
|
||||
<Link to={`/manage/users/${tag.created_by_user.id}`}>
|
||||
@{tag.created_by_user.username}
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
<Link to={`/manage/tags/${tag.id}`}>{Tr("Edit")}</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{!showAddTag && (
|
||||
<button onClick={() => setShowAddTag(true)}>{Tr("Add tag")}</button>
|
||||
)}
|
||||
{showAddTag && (
|
||||
<div>
|
||||
<label htmlFor="newTagName">{Tr("New Tag Name")}</label>
|
||||
<input
|
||||
type="text"
|
||||
id="newTagName"
|
||||
value={newTagName}
|
||||
onChange={(e) => setNewTagName(e.target.value)}
|
||||
/>
|
||||
<label htmlFor="newTagDescription">{Tr("New Tag Description")}</label>
|
||||
<textarea
|
||||
id="newTagDescription"
|
||||
value={newTagDescription}
|
||||
onChange={(e) => setNewTagDescription(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
fetch("/api/v1/insert_tag", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: newTagName,
|
||||
description: newTagDescription,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
} else {
|
||||
setNewTagName("");
|
||||
setNewTagDescription("");
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{Tr("Create tag")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Tags;
|
||||
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;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user