v0.1
This commit is contained in:
parent
90e6ba5070
commit
1bb57bc94a
@ -15,3 +15,19 @@ type User struct {
|
|||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Video struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Images string `json:"images"`
|
||||||
|
OriginLink string `json:"origin_link"`
|
||||||
|
PlayLink string `json:"play_link"`
|
||||||
|
Status int32 `json:"status"`
|
||||||
|
IsDeleted bool `json:"is_deleted"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
CreateAt time.Time `json:"create_at"`
|
||||||
|
CreateBy string `json:"create_by"`
|
||||||
|
UpdateAt time.Time `json:"update_at"`
|
||||||
|
UpdateBy string `json:"update_by"`
|
||||||
|
}
|
||||||
|
|||||||
@ -10,12 +10,20 @@ import (
|
|||||||
|
|
||||||
type Querier interface {
|
type Querier interface {
|
||||||
CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
|
CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
|
||||||
|
CreateVideo(ctx context.Context, arg CreateVideoParams) (Video, error)
|
||||||
DeleteUser(ctx context.Context, id string) error
|
DeleteUser(ctx context.Context, id string) error
|
||||||
|
DeleteVideo(ctx context.Context, id string) error
|
||||||
GetUser(ctx context.Context, id string) (User, error)
|
GetUser(ctx context.Context, id string) (User, error)
|
||||||
GetUserByEmail(ctx context.Context, email string) (User, error)
|
GetUserByEmail(ctx context.Context, email string) (User, error)
|
||||||
GetUserByName(ctx context.Context, username string) (User, error)
|
GetUserByName(ctx context.Context, username string) (User, error)
|
||||||
|
GetVideo(ctx context.Context, id string) (Video, error)
|
||||||
ListUsers(ctx context.Context, arg ListUsersParams) ([]User, error)
|
ListUsers(ctx context.Context, arg ListUsersParams) ([]User, error)
|
||||||
|
ListVideos(ctx context.Context, arg ListVideosParams) ([]Video, error)
|
||||||
|
ListVideosByUser(ctx context.Context, arg ListVideosByUserParams) ([]Video, error)
|
||||||
|
SetVideoPlay(ctx context.Context, arg SetVideoPlayParams) (Video, error)
|
||||||
UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error)
|
UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error)
|
||||||
|
UpdateVideo(ctx context.Context, arg UpdateVideoParams) (Video, error)
|
||||||
|
UpdateVideoStatus(ctx context.Context, arg UpdateVideoStatusParams) (Video, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Querier = (*Queries)(nil)
|
var _ Querier = (*Queries)(nil)
|
||||||
|
|||||||
58
internal/db/query/video.sql
Normal file
58
internal/db/query/video.sql
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
-- name: CreateVideo :one
|
||||||
|
INSERT INTO videos (
|
||||||
|
id, title, description, images, origin_link, play_link, user_id, create_by
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3, $4, $5, $6, $7, $8
|
||||||
|
)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: DeleteVideo :exec
|
||||||
|
UPDATE videos
|
||||||
|
SET is_deleted = TRUE
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: UpdateVideoStatus :one
|
||||||
|
UPDATE videos
|
||||||
|
SET status = $2,
|
||||||
|
update_at = $3,
|
||||||
|
update_by = $4
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: SetVideoPlay :one
|
||||||
|
UPDATE videos
|
||||||
|
SET status = $2,
|
||||||
|
play_link = $3,
|
||||||
|
update_at = $4,
|
||||||
|
update_by = $5
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: UpdateVideo :one
|
||||||
|
UPDATE videos
|
||||||
|
SET title = $2,
|
||||||
|
description = $3,
|
||||||
|
images = $4,
|
||||||
|
status = $5,
|
||||||
|
update_at = $6,
|
||||||
|
update_by = $7
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: GetVideo :one
|
||||||
|
SELECT * FROM videos
|
||||||
|
WHERE id = $1 LIMIT 1;
|
||||||
|
|
||||||
|
-- name: ListVideos :many
|
||||||
|
SELECT * FROM videos
|
||||||
|
WHERE is_deleted = FALSE AND status=200
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT $1
|
||||||
|
OFFSET $2;
|
||||||
|
|
||||||
|
-- name: ListVideosByUser :many
|
||||||
|
SELECT * FROM videos
|
||||||
|
WHERE is_deleted = FALSE AND user_id = $1
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT $2
|
||||||
|
OFFSET $3;
|
||||||
@ -1 +1,2 @@
|
|||||||
|
DROP TABLE "videos";
|
||||||
DROP TABLE "users";
|
DROP TABLE "users";
|
||||||
@ -1,7 +1,39 @@
|
|||||||
CREATE TABLE "users" (
|
CREATE TABLE "users" (
|
||||||
"id" varchar NOT NULL PRIMARY KEY,
|
"id" varchar NOT NULL PRIMARY KEY,
|
||||||
"username" varchar NOT NULL UNIQUE,
|
"username" varchar NOT NULL,
|
||||||
"hashed_password" varchar NOT NULL,
|
"hashed_password" varchar NOT NULL,
|
||||||
"email" varchar NOT NULL UNIQUE,
|
"email" varchar NOT NULL,
|
||||||
"created_at" timestamptz NOT NULL DEFAULT (now())
|
"created_at" timestamptz NOT NULL DEFAULT (now())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ALTER TABLE "users" ADD CONSTRAINT "username_key" UNIQUE ("username");
|
||||||
|
ALTER TABLE "users" ADD CONSTRAINT "email_key" UNIQUE ("email");
|
||||||
|
CREATE INDEX ON "users" ("username");
|
||||||
|
CREATE INDEX ON "users" ("email");
|
||||||
|
|
||||||
|
CREATE TABLE "videos" (
|
||||||
|
"id" varchar NOT NULL PRIMARY KEY,
|
||||||
|
"title" varchar NOT NULL,
|
||||||
|
"description" varchar NOT NULL,
|
||||||
|
"images" varchar NOT NULL,
|
||||||
|
"origin_link" varchar NOT NULL,
|
||||||
|
"play_link" varchar NOT NULL,
|
||||||
|
-- 100: 下架
|
||||||
|
-- 0: 添加视频
|
||||||
|
-- 1: 视频转码中
|
||||||
|
-- 2: 视频转码失败
|
||||||
|
-- 200: 视频正常显示播放
|
||||||
|
"status" int NOT NULL DEFAULT (0),
|
||||||
|
"is_deleted" boolean NOT NULL DEFAULT false, -- 删除
|
||||||
|
"user_id" varchar NOT NULL,
|
||||||
|
"create_at" timestamptz NOT NULL DEFAULT (now()),
|
||||||
|
"create_by" varchar NOT NULL,
|
||||||
|
"update_at" timestamptz NOT NULL DEFAULT('0001-01-01 00:00:00Z'),
|
||||||
|
"update_by" varchar NOT NULL DEFAULT ('')
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE "videos" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("id");
|
||||||
|
CREATE INDEX ON "videos" ("status");
|
||||||
|
CREATE INDEX ON "videos" ("user_id", "status", "create_at");
|
||||||
|
CREATE INDEX ON "videos" ("title");
|
||||||
|
CREATE INDEX ON "videos" ("user_id", "title");
|
||||||
337
internal/db/video.sql.go
Normal file
337
internal/db/video.sql.go
Normal file
@ -0,0 +1,337 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.24.0
|
||||||
|
// source: video.sql
|
||||||
|
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createVideo = `-- name: CreateVideo :one
|
||||||
|
INSERT INTO videos (
|
||||||
|
id, title, description, images, origin_link, play_link, user_id, create_by
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3, $4, $5, $6, $7, $8
|
||||||
|
)
|
||||||
|
RETURNING id, title, description, images, origin_link, play_link, status, is_deleted, user_id, create_at, create_by, update_at, update_by
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateVideoParams struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Images string `json:"images"`
|
||||||
|
OriginLink string `json:"origin_link"`
|
||||||
|
PlayLink string `json:"play_link"`
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
CreateBy string `json:"create_by"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateVideo(ctx context.Context, arg CreateVideoParams) (Video, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, createVideo,
|
||||||
|
arg.ID,
|
||||||
|
arg.Title,
|
||||||
|
arg.Description,
|
||||||
|
arg.Images,
|
||||||
|
arg.OriginLink,
|
||||||
|
arg.PlayLink,
|
||||||
|
arg.UserID,
|
||||||
|
arg.CreateBy,
|
||||||
|
)
|
||||||
|
var i Video
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Title,
|
||||||
|
&i.Description,
|
||||||
|
&i.Images,
|
||||||
|
&i.OriginLink,
|
||||||
|
&i.PlayLink,
|
||||||
|
&i.Status,
|
||||||
|
&i.IsDeleted,
|
||||||
|
&i.UserID,
|
||||||
|
&i.CreateAt,
|
||||||
|
&i.CreateBy,
|
||||||
|
&i.UpdateAt,
|
||||||
|
&i.UpdateBy,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteVideo = `-- name: DeleteVideo :exec
|
||||||
|
UPDATE videos
|
||||||
|
SET is_deleted = TRUE
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) DeleteVideo(ctx context.Context, id string) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, deleteVideo, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getVideo = `-- name: GetVideo :one
|
||||||
|
SELECT id, title, description, images, origin_link, play_link, status, is_deleted, user_id, create_at, create_by, update_at, update_by FROM videos
|
||||||
|
WHERE id = $1 LIMIT 1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetVideo(ctx context.Context, id string) (Video, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getVideo, id)
|
||||||
|
var i Video
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Title,
|
||||||
|
&i.Description,
|
||||||
|
&i.Images,
|
||||||
|
&i.OriginLink,
|
||||||
|
&i.PlayLink,
|
||||||
|
&i.Status,
|
||||||
|
&i.IsDeleted,
|
||||||
|
&i.UserID,
|
||||||
|
&i.CreateAt,
|
||||||
|
&i.CreateBy,
|
||||||
|
&i.UpdateAt,
|
||||||
|
&i.UpdateBy,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const listVideos = `-- name: ListVideos :many
|
||||||
|
SELECT id, title, description, images, origin_link, play_link, status, is_deleted, user_id, create_at, create_by, update_at, update_by FROM videos
|
||||||
|
WHERE is_deleted = FALSE AND status=200
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT $1
|
||||||
|
OFFSET $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListVideosParams struct {
|
||||||
|
Limit int32 `json:"limit"`
|
||||||
|
Offset int32 `json:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListVideos(ctx context.Context, arg ListVideosParams) ([]Video, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, listVideos, arg.Limit, arg.Offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []Video{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i Video
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Title,
|
||||||
|
&i.Description,
|
||||||
|
&i.Images,
|
||||||
|
&i.OriginLink,
|
||||||
|
&i.PlayLink,
|
||||||
|
&i.Status,
|
||||||
|
&i.IsDeleted,
|
||||||
|
&i.UserID,
|
||||||
|
&i.CreateAt,
|
||||||
|
&i.CreateBy,
|
||||||
|
&i.UpdateAt,
|
||||||
|
&i.UpdateBy,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const listVideosByUser = `-- name: ListVideosByUser :many
|
||||||
|
SELECT id, title, description, images, origin_link, play_link, status, is_deleted, user_id, create_at, create_by, update_at, update_by FROM videos
|
||||||
|
WHERE is_deleted = FALSE AND user_id = $1
|
||||||
|
ORDER BY id DESC
|
||||||
|
LIMIT $2
|
||||||
|
OFFSET $3
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListVideosByUserParams struct {
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
Limit int32 `json:"limit"`
|
||||||
|
Offset int32 `json:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListVideosByUser(ctx context.Context, arg ListVideosByUserParams) ([]Video, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, listVideosByUser, arg.UserID, arg.Limit, arg.Offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []Video{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i Video
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Title,
|
||||||
|
&i.Description,
|
||||||
|
&i.Images,
|
||||||
|
&i.OriginLink,
|
||||||
|
&i.PlayLink,
|
||||||
|
&i.Status,
|
||||||
|
&i.IsDeleted,
|
||||||
|
&i.UserID,
|
||||||
|
&i.CreateAt,
|
||||||
|
&i.CreateBy,
|
||||||
|
&i.UpdateAt,
|
||||||
|
&i.UpdateBy,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const setVideoPlay = `-- name: SetVideoPlay :one
|
||||||
|
UPDATE videos
|
||||||
|
SET status = $2,
|
||||||
|
play_link = $3,
|
||||||
|
update_at = $4,
|
||||||
|
update_by = $5
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING id, title, description, images, origin_link, play_link, status, is_deleted, user_id, create_at, create_by, update_at, update_by
|
||||||
|
`
|
||||||
|
|
||||||
|
type SetVideoPlayParams struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Status int32 `json:"status"`
|
||||||
|
PlayLink string `json:"play_link"`
|
||||||
|
UpdateAt time.Time `json:"update_at"`
|
||||||
|
UpdateBy string `json:"update_by"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) SetVideoPlay(ctx context.Context, arg SetVideoPlayParams) (Video, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, setVideoPlay,
|
||||||
|
arg.ID,
|
||||||
|
arg.Status,
|
||||||
|
arg.PlayLink,
|
||||||
|
arg.UpdateAt,
|
||||||
|
arg.UpdateBy,
|
||||||
|
)
|
||||||
|
var i Video
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Title,
|
||||||
|
&i.Description,
|
||||||
|
&i.Images,
|
||||||
|
&i.OriginLink,
|
||||||
|
&i.PlayLink,
|
||||||
|
&i.Status,
|
||||||
|
&i.IsDeleted,
|
||||||
|
&i.UserID,
|
||||||
|
&i.CreateAt,
|
||||||
|
&i.CreateBy,
|
||||||
|
&i.UpdateAt,
|
||||||
|
&i.UpdateBy,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateVideo = `-- name: UpdateVideo :one
|
||||||
|
UPDATE videos
|
||||||
|
SET title = $2,
|
||||||
|
description = $3,
|
||||||
|
images = $4,
|
||||||
|
status = $5,
|
||||||
|
update_at = $6,
|
||||||
|
update_by = $7
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING id, title, description, images, origin_link, play_link, status, is_deleted, user_id, create_at, create_by, update_at, update_by
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateVideoParams struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Images string `json:"images"`
|
||||||
|
Status int32 `json:"status"`
|
||||||
|
UpdateAt time.Time `json:"update_at"`
|
||||||
|
UpdateBy string `json:"update_by"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateVideo(ctx context.Context, arg UpdateVideoParams) (Video, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, updateVideo,
|
||||||
|
arg.ID,
|
||||||
|
arg.Title,
|
||||||
|
arg.Description,
|
||||||
|
arg.Images,
|
||||||
|
arg.Status,
|
||||||
|
arg.UpdateAt,
|
||||||
|
arg.UpdateBy,
|
||||||
|
)
|
||||||
|
var i Video
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Title,
|
||||||
|
&i.Description,
|
||||||
|
&i.Images,
|
||||||
|
&i.OriginLink,
|
||||||
|
&i.PlayLink,
|
||||||
|
&i.Status,
|
||||||
|
&i.IsDeleted,
|
||||||
|
&i.UserID,
|
||||||
|
&i.CreateAt,
|
||||||
|
&i.CreateBy,
|
||||||
|
&i.UpdateAt,
|
||||||
|
&i.UpdateBy,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateVideoStatus = `-- name: UpdateVideoStatus :one
|
||||||
|
UPDATE videos
|
||||||
|
SET status = $2,
|
||||||
|
update_at = $3,
|
||||||
|
update_by = $4
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING id, title, description, images, origin_link, play_link, status, is_deleted, user_id, create_at, create_by, update_at, update_by
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateVideoStatusParams struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Status int32 `json:"status"`
|
||||||
|
UpdateAt time.Time `json:"update_at"`
|
||||||
|
UpdateBy string `json:"update_by"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateVideoStatus(ctx context.Context, arg UpdateVideoStatusParams) (Video, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, updateVideoStatus,
|
||||||
|
arg.ID,
|
||||||
|
arg.Status,
|
||||||
|
arg.UpdateAt,
|
||||||
|
arg.UpdateBy,
|
||||||
|
)
|
||||||
|
var i Video
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Title,
|
||||||
|
&i.Description,
|
||||||
|
&i.Images,
|
||||||
|
&i.OriginLink,
|
||||||
|
&i.PlayLink,
|
||||||
|
&i.Status,
|
||||||
|
&i.IsDeleted,
|
||||||
|
&i.UserID,
|
||||||
|
&i.CreateAt,
|
||||||
|
&i.CreateBy,
|
||||||
|
&i.UpdateAt,
|
||||||
|
&i.UpdateBy,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
@ -14,9 +14,9 @@ const (
|
|||||||
|
|
||||||
type CtxTypeUser string
|
type CtxTypeUser string
|
||||||
|
|
||||||
type authorize struct {
|
type Authorize struct {
|
||||||
AuthID string `json:"auth_id"`
|
ID string `json:"id"`
|
||||||
AuthName string `json:"auth_name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func genId() string {
|
func genId() string {
|
||||||
|
|||||||
@ -1,20 +1,39 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/zhang2092/mediahls/internal/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
type pageData struct {
|
type pageData struct {
|
||||||
AuthID string
|
Authorize
|
||||||
AuthName string
|
Videos []db.Video
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) home(w http.ResponseWriter, r *http.Request) {
|
func (server *Server) home(w http.ResponseWriter, r *http.Request) {
|
||||||
pd := pageData{}
|
pd := pageData{}
|
||||||
auth, err := server.withCookie(r)
|
auth, err := server.withCookie(r)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
pd.AuthID = auth.AuthID
|
pd.Authorize = *auth
|
||||||
pd.AuthName = auth.AuthName
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
videos, err := server.store.ListVideos(ctx, db.ListVideosParams{
|
||||||
|
Limit: 100,
|
||||||
|
Offset: 0,
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
for _, item := range videos {
|
||||||
|
if len(item.Description) > 65 {
|
||||||
|
temp := strings.TrimSpace(item.Description[0:65]) + "..."
|
||||||
|
item.Description = temp
|
||||||
|
log.Println(item.Description)
|
||||||
|
}
|
||||||
|
pd.Videos = append(pd.Videos, item)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
renderHome(w, pd)
|
renderHome(w, pd)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,14 +30,14 @@ func (server *Server) authorizeMiddleware(next http.Handler) http.Handler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) withCookie(r *http.Request) (*authorize, error) {
|
func (server *Server) withCookie(r *http.Request) (*Authorize, error) {
|
||||||
cookie, err := r.Cookie(AuthorizeCookie)
|
cookie, err := r.Cookie(AuthorizeCookie)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("get cookie: %v", err)
|
log.Printf("get cookie: %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
u := &authorize{}
|
u := &Authorize{}
|
||||||
err = server.secureCookie.Decode(AuthorizeCookie, cookie.Value, u)
|
err = server.secureCookie.Decode(AuthorizeCookie, cookie.Value, u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("secure decode cookie: %v", err)
|
log.Printf("secure decode cookie: %v", err)
|
||||||
@ -47,19 +47,13 @@ func (server *Server) withCookie(r *http.Request) (*authorize, error) {
|
|||||||
return u, nil
|
return u, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func withUser(ctx context.Context) *authorize {
|
func withUser(ctx context.Context) Authorize {
|
||||||
var result authorize
|
var result Authorize
|
||||||
ctxValue, err := convert.ToByteE(ctx.Value(ContextUser))
|
ctxValue, err := convert.ToByteE(ctx.Value(ContextUser))
|
||||||
log.Printf("ctx: %s", ctxValue)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("1: %v", err)
|
return result
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err = json.Unmarshal(ctxValue, &result)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("2: %v", err)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &result
|
json.Unmarshal(ctxValue, &result)
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,6 +55,7 @@ func (server *Server) setupRouter() {
|
|||||||
router := mux.NewRouter()
|
router := mux.NewRouter()
|
||||||
router.Use(mux.CORSMethodMiddleware(router))
|
router.Use(mux.CORSMethodMiddleware(router))
|
||||||
router.PathPrefix("/statics/").Handler(http.StripPrefix("/statics/", http.FileServer(http.Dir("web/statics"))))
|
router.PathPrefix("/statics/").Handler(http.StripPrefix("/statics/", http.FileServer(http.Dir("web/statics"))))
|
||||||
|
router.PathPrefix("/upload/imgs").Handler(http.StripPrefix("/upload/imgs/", http.FileServer(http.Dir("upload/imgs"))))
|
||||||
|
|
||||||
router.HandleFunc("/register", server.registerView).Methods(http.MethodGet)
|
router.HandleFunc("/register", server.registerView).Methods(http.MethodGet)
|
||||||
router.HandleFunc("/register", server.register).Methods(http.MethodPost)
|
router.HandleFunc("/register", server.register).Methods(http.MethodPost)
|
||||||
@ -68,8 +69,17 @@ func (server *Server) setupRouter() {
|
|||||||
|
|
||||||
subRouter := router.PathPrefix("/").Subrouter()
|
subRouter := router.PathPrefix("/").Subrouter()
|
||||||
subRouter.Use(server.authorizeMiddleware)
|
subRouter.Use(server.authorizeMiddleware)
|
||||||
subRouter.HandleFunc("/upload", server.uploadView).Methods(http.MethodGet)
|
subRouter.HandleFunc("/me/videos", server.videosView).Methods(http.MethodGet)
|
||||||
subRouter.HandleFunc("/upload", server.upload).Methods(http.MethodPost)
|
subRouter.HandleFunc("/me/videos/p{page}", server.videosView).Methods(http.MethodGet)
|
||||||
|
|
||||||
|
subRouter.HandleFunc("/me/videos/create", server.createVideoView).Methods(http.MethodGet)
|
||||||
|
subRouter.HandleFunc("/me/videos/create/{xid}", server.createVideoView).Methods(http.MethodGet)
|
||||||
|
subRouter.HandleFunc("/me/videos/create", server.createVideo).Methods(http.MethodPost)
|
||||||
|
|
||||||
|
subRouter.HandleFunc("/upload_image", server.uploadImage).Methods(http.MethodPost)
|
||||||
|
subRouter.HandleFunc("/upload_file", server.uploadVideo).Methods(http.MethodPost)
|
||||||
|
subRouter.HandleFunc("/transfer/{xid}", server.transferView).Methods(http.MethodGet)
|
||||||
|
subRouter.HandleFunc("/transfer/{xid}", server.transfer).Methods(http.MethodPost)
|
||||||
|
|
||||||
server.router = router
|
server.router = router
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -13,17 +15,7 @@ import (
|
|||||||
"github.com/zhang2092/mediahls/internal/pkg/fileutil"
|
"github.com/zhang2092/mediahls/internal/pkg/fileutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (server *Server) uploadView(w http.ResponseWriter, r *http.Request) {
|
func (server *Server) uploadVideo(w http.ResponseWriter, r *http.Request) {
|
||||||
user := withUser(r.Context())
|
|
||||||
log.Printf("%v", user)
|
|
||||||
renderUpload(w, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderUpload(w http.ResponseWriter, data any) {
|
|
||||||
render(w, data, "web/templates/me/upload.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (server *Server) upload(w http.ResponseWriter, r *http.Request) {
|
|
||||||
defer r.Body.Close()
|
defer r.Body.Close()
|
||||||
|
|
||||||
file, fileHeader, err := r.FormFile("file")
|
file, fileHeader, err := r.FormFile("file")
|
||||||
@ -56,7 +48,8 @@ func (server *Server) upload(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dir := path.Join("upload", time.Now().Format("20060102"))
|
curTime := time.Now()
|
||||||
|
dir := path.Join("upload", "files", curTime.Format("2006"), curTime.Format("01"), curTime.Format("02"))
|
||||||
exist, _ := fileutil.PathExists(dir)
|
exist, _ := fileutil.PathExists(dir)
|
||||||
if !exist {
|
if !exist {
|
||||||
err := os.MkdirAll(dir, os.ModePerm)
|
err := os.MkdirAll(dir, os.ModePerm)
|
||||||
@ -87,6 +80,60 @@ func (server *Server) upload(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, err = w.Write([]byte("/" + filePath))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("%v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (server *Server) uploadImage(w http.ResponseWriter, r *http.Request) {
|
||||||
|
defer func(Body io.ReadCloser) {
|
||||||
|
_ = Body.Close()
|
||||||
|
}(r.Body)
|
||||||
|
|
||||||
|
_, fh, err := r.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
_, err = w.Write([]byte(err.Error()))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("%v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := fh.Open()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("%v", err)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
_, err = w.Write([]byte("读取图片失败"))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("%v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reader := bufio.NewReader(f)
|
||||||
|
filePath, err := fileutil.UploadImage(reader)
|
||||||
|
if errors.Is(err, fileutil.ErrUnsupportedFileFormat) {
|
||||||
|
log.Printf("%v", err)
|
||||||
|
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||||
|
_, err = w.Write([]byte(fileutil.ErrUnsupportedFileFormat.Error()))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("%v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("%v", err)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
_, err = w.Write([]byte(err.Error()))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("%v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
_, err = w.Write([]byte(filePath))
|
_, err = w.Write([]byte(filePath))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -61,6 +61,7 @@ func viladatorRegister(email, username, password string) (*respErrs, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type respErrs struct {
|
type respErrs struct {
|
||||||
|
Authorize
|
||||||
Summary string
|
Summary string
|
||||||
Email string
|
Email string
|
||||||
Username string
|
Username string
|
||||||
@ -191,7 +192,7 @@ func (server *Server) login(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
encoded, err := server.secureCookie.Encode(AuthorizeCookie, &authorize{AuthID: user.ID, AuthName: user.Username})
|
encoded, err := server.secureCookie.Encode(AuthorizeCookie, &Authorize{ID: user.ID, Name: user.Username})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs.Summary = "请求网络错误,请刷新重试(cookie)"
|
errs.Summary = "请求网络错误,请刷新重试(cookie)"
|
||||||
renderLogin(w, errs)
|
renderLogin(w, errs)
|
||||||
|
|||||||
@ -1,28 +1,36 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/zhang2092/mediahls/internal/db"
|
||||||
|
"github.com/zhang2092/mediahls/internal/pkg/convert"
|
||||||
|
"github.com/zhang2092/mediahls/internal/pkg/fileutil"
|
||||||
|
"github.com/zhang2092/mediahls/internal/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
type playData struct {
|
type playData struct {
|
||||||
AuthID string
|
Authorize
|
||||||
AuthName string
|
Url string
|
||||||
Url string
|
Video db.Video
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) play(w http.ResponseWriter, r *http.Request) {
|
func (server *Server) play(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
xid := vars["xid"]
|
xid := vars["xid"]
|
||||||
|
video, _ := server.store.GetVideo(r.Context(), xid)
|
||||||
data := playData{
|
data := playData{
|
||||||
Url: "/media/" + xid + "/stream/",
|
Video: video,
|
||||||
}
|
}
|
||||||
auth, err := server.withCookie(r)
|
auth, err := server.withCookie(r)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
data.AuthID = auth.AuthID
|
data.Authorize = *auth
|
||||||
data.AuthName = auth.AuthName
|
|
||||||
}
|
}
|
||||||
render(w, data, "web/templates/video/play.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
|
render(w, data, "web/templates/video/play.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
|
||||||
}
|
}
|
||||||
@ -43,10 +51,7 @@ http.ServeContent(w, r, "git", time.Now(), video)
|
|||||||
func (server *Server) stream(response http.ResponseWriter, request *http.Request) {
|
func (server *Server) stream(response http.ResponseWriter, request *http.Request) {
|
||||||
vars := mux.Vars(request)
|
vars := mux.Vars(request)
|
||||||
mId := vars["xid"]
|
mId := vars["xid"]
|
||||||
fmt.Println(mId)
|
|
||||||
|
|
||||||
segName, ok := vars["segName"]
|
segName, ok := vars["segName"]
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
mediaBase := getMediaBase(mId)
|
mediaBase := getMediaBase(mId)
|
||||||
m3u8Name := "index.m3u8"
|
m3u8Name := "index.m3u8"
|
||||||
@ -55,29 +60,261 @@ func (server *Server) stream(response http.ResponseWriter, request *http.Request
|
|||||||
mediaBase := getMediaBase(mId)
|
mediaBase := getMediaBase(mId)
|
||||||
serveHlsTs(response, request, mediaBase, segName)
|
serveHlsTs(response, request, mediaBase, segName)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMediaBase(mId string) string {
|
func getMediaBase(mId string) string {
|
||||||
mediaRoot := "media"
|
mediaRoot := "media"
|
||||||
return fmt.Sprintf("%s/%s", mediaRoot, mId)
|
return fmt.Sprintf("%s/%s", mediaRoot, mId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveHlsM3u8(w http.ResponseWriter, r *http.Request, mediaBase, m3u8Name string) {
|
func serveHlsM3u8(w http.ResponseWriter, r *http.Request, mediaBase, m3u8Name string) {
|
||||||
fmt.Println("serveHlsM3u8...")
|
|
||||||
|
|
||||||
mediaFile := fmt.Sprintf("%s/%s", mediaBase, m3u8Name)
|
mediaFile := fmt.Sprintf("%s/%s", mediaBase, m3u8Name)
|
||||||
fmt.Println(mediaFile)
|
|
||||||
http.ServeFile(w, r, mediaFile)
|
http.ServeFile(w, r, mediaFile)
|
||||||
w.Header().Set("Content-Type", "application/x-mpegURL")
|
w.Header().Set("Content-Type", "application/x-mpegURL")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveHlsTs(w http.ResponseWriter, r *http.Request, mediaBase, segName string) {
|
func serveHlsTs(w http.ResponseWriter, r *http.Request, mediaBase, segName string) {
|
||||||
fmt.Println("serveHlsTs...")
|
|
||||||
|
|
||||||
mediaFile := fmt.Sprintf("%s/%s", mediaBase, segName)
|
mediaFile := fmt.Sprintf("%s/%s", mediaBase, segName)
|
||||||
fmt.Println(mediaFile)
|
|
||||||
|
|
||||||
http.ServeFile(w, r, mediaFile)
|
http.ServeFile(w, r, mediaFile)
|
||||||
w.Header().Set("Content-Type", "video/MP2T")
|
w.Header().Set("Content-Type", "video/MP2T")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type meVideoData struct {
|
||||||
|
Authorize
|
||||||
|
Videos []db.Video
|
||||||
|
}
|
||||||
|
|
||||||
|
func (server *Server) videosView(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
data := meVideoData{
|
||||||
|
Authorize: withUser(ctx),
|
||||||
|
}
|
||||||
|
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
page, err := strconv.Atoi(vars["page"])
|
||||||
|
if err != nil {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
videos, err := server.store.ListVideosByUser(ctx, db.ListVideosByUserParams{
|
||||||
|
UserID: data.Authorize.ID,
|
||||||
|
Limit: 16,
|
||||||
|
Offset: int32((page - 1) * 16),
|
||||||
|
})
|
||||||
|
if err == nil {
|
||||||
|
for _, item := range videos {
|
||||||
|
if len(item.Description) > 65 {
|
||||||
|
temp := strings.TrimSpace(item.Description[0:65]) + "..."
|
||||||
|
item.Description = temp
|
||||||
|
}
|
||||||
|
data.Videos = append(data.Videos, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(w, data, "web/templates/me/videos.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (server *Server) createVideoView(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
xid := vars["xid"]
|
||||||
|
vm := videoCreateResp{
|
||||||
|
Authorize: withUser(r.Context()),
|
||||||
|
}
|
||||||
|
if len(xid) > 0 {
|
||||||
|
if v, err := server.store.GetVideo(r.Context(), xid); err == nil {
|
||||||
|
vm.ID = v.ID
|
||||||
|
vm.Title = v.Title
|
||||||
|
vm.Images = v.Images
|
||||||
|
vm.Description = v.Description
|
||||||
|
vm.OriginLink = v.OriginLink
|
||||||
|
vm.Status = int(v.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderCreateVideo(w, vm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderCreateVideo(w http.ResponseWriter, data any) {
|
||||||
|
render(w, data, "web/templates/video/edit.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
|
||||||
|
}
|
||||||
|
|
||||||
|
type videoCreateResp struct {
|
||||||
|
Authorize
|
||||||
|
Summary string
|
||||||
|
ID string
|
||||||
|
IDErr string
|
||||||
|
Title string
|
||||||
|
TitleErr string
|
||||||
|
Images string
|
||||||
|
ImagesErr string
|
||||||
|
Description string
|
||||||
|
DescriptionErr string
|
||||||
|
OriginLink string
|
||||||
|
OriginLinkErr string
|
||||||
|
Status int
|
||||||
|
StatusErr string
|
||||||
|
}
|
||||||
|
|
||||||
|
func viladatorCreateVedio(r *http.Request) (*videoCreateResp, bool) {
|
||||||
|
ok := true
|
||||||
|
status, _ := strconv.Atoi(r.PostFormValue("status"))
|
||||||
|
errs := &videoCreateResp{
|
||||||
|
Authorize: withUser(r.Context()),
|
||||||
|
ID: r.PostFormValue("id"),
|
||||||
|
Title: r.PostFormValue("title"),
|
||||||
|
Images: r.PostFormValue("images"),
|
||||||
|
Description: r.PostFormValue("description"),
|
||||||
|
OriginLink: r.PostFormValue("origin_link"),
|
||||||
|
Status: status,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs.Title) == 0 {
|
||||||
|
errs.TitleErr = "请填写正确的标题"
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
|
||||||
|
exist, _ := fileutil.PathExists(strings.TrimPrefix(errs.Images, "/"))
|
||||||
|
if !exist {
|
||||||
|
errs.ImagesErr = "请先上传图片"
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs.Description) == 0 {
|
||||||
|
errs.DescriptionErr = "请填写描述"
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
|
||||||
|
exist, _ = fileutil.PathExists(strings.TrimPrefix(errs.OriginLink, "/"))
|
||||||
|
if !exist {
|
||||||
|
errs.OriginLinkErr = "请先上传视频"
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return errs, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (server *Server) createVideo(w http.ResponseWriter, r *http.Request) {
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
renderCreateVideo(w, videoCreateResp{Summary: "请求网络错误, 请刷新重试"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
vm, ok := viladatorCreateVedio(r)
|
||||||
|
if !ok {
|
||||||
|
renderCreateVideo(w, vm)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
curTime := time.Now()
|
||||||
|
ctx := r.Context()
|
||||||
|
u := withUser(ctx)
|
||||||
|
if len(vm.ID) == 0 {
|
||||||
|
_, err := server.store.CreateVideo(ctx, db.CreateVideoParams{
|
||||||
|
ID: genId(),
|
||||||
|
Title: vm.Title,
|
||||||
|
Description: vm.Description,
|
||||||
|
Images: vm.Images,
|
||||||
|
OriginLink: vm.OriginLink,
|
||||||
|
PlayLink: "",
|
||||||
|
UserID: u.ID,
|
||||||
|
CreateBy: u.Name,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
vm.Summary = "添加视频失败"
|
||||||
|
renderCreateVideo(w, vm)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, err := server.store.UpdateVideo(ctx, db.UpdateVideoParams{
|
||||||
|
ID: vm.ID,
|
||||||
|
Title: vm.Title,
|
||||||
|
Description: vm.Description,
|
||||||
|
Images: vm.Images,
|
||||||
|
Status: int32(vm.Status),
|
||||||
|
UpdateAt: curTime,
|
||||||
|
UpdateBy: u.Name,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
vm.Summary = "更新视频失败"
|
||||||
|
renderCreateVideo(w, vm)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/me/videos", http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
type transferData struct {
|
||||||
|
Authorize
|
||||||
|
Video db.Video
|
||||||
|
}
|
||||||
|
|
||||||
|
func (server *Server) transferView(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
xid := vars["xid"]
|
||||||
|
v, _ := server.store.GetVideo(r.Context(), xid)
|
||||||
|
data := transferData{
|
||||||
|
Video: v,
|
||||||
|
}
|
||||||
|
u, err := server.withCookie(r)
|
||||||
|
if err == nil {
|
||||||
|
data.Authorize = *u
|
||||||
|
}
|
||||||
|
render(w, data, "web/templates/video/transfer.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (server *Server) transfer(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
xid := vars["xid"]
|
||||||
|
ctx := r.Context()
|
||||||
|
v, err := server.store.GetVideo(ctx, xid)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "视频信息错误", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
u := withUser(ctx)
|
||||||
|
v, err = server.store.UpdateVideoStatus(ctx, db.UpdateVideoStatusParams{
|
||||||
|
ID: v.ID,
|
||||||
|
Status: 1,
|
||||||
|
UpdateAt: time.Now(),
|
||||||
|
UpdateBy: u.Name,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "视频转码错误", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func(v db.Video, name string) {
|
||||||
|
ctx := context.Background()
|
||||||
|
err := convert.ConvertHLS("media/"+v.ID+"/", strings.TrimPrefix(v.OriginLink, "/"))
|
||||||
|
if err != nil {
|
||||||
|
logger.Logger.Errorf("Convert HLS [%s]-[%s]: %v", v.ID, v.OriginLink, err)
|
||||||
|
_, _ = server.store.UpdateVideoStatus(ctx, db.UpdateVideoStatusParams{
|
||||||
|
ID: v.ID,
|
||||||
|
Status: 2,
|
||||||
|
UpdateAt: time.Now(),
|
||||||
|
UpdateBy: name,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转码成功
|
||||||
|
if _, err = server.store.SetVideoPlay(ctx, db.SetVideoPlayParams{
|
||||||
|
ID: v.ID,
|
||||||
|
Status: 200,
|
||||||
|
PlayLink: "/media/" + v.ID + "/stream/",
|
||||||
|
UpdateAt: time.Now(),
|
||||||
|
UpdateBy: name,
|
||||||
|
}); err != nil {
|
||||||
|
logger.Logger.Errorf("Set Video Play [%s]-[%s]: %v", v.ID, v.OriginLink, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Logger.Infof("[%s]-[%s] 转码完成", v.ID, v.OriginLink)
|
||||||
|
}(v, u.Name)
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("视频正在转码中, 请稍后刷新页面"))
|
||||||
|
}
|
||||||
|
|||||||
@ -1,6 +1,28 @@
|
|||||||
package fileutil
|
package fileutil
|
||||||
|
|
||||||
import "os"
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/gif"
|
||||||
|
"image/jpeg"
|
||||||
|
"image/png"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
nanoId "github.com/matoous/go-nanoid"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// 图片最大数量
|
||||||
|
MaxImageSize = 10 << 20
|
||||||
|
ErrUnsupportedFileFormat = errors.New("图片格式不支持")
|
||||||
|
ErrFileStorePath = errors.New("文件存储路径异常")
|
||||||
|
ErrFileGenerateName = errors.New("文件名称生成异常")
|
||||||
|
ErrFileSaveFailed = errors.New("文件保存失败")
|
||||||
|
)
|
||||||
|
|
||||||
func PathExists(path string) (bool, error) {
|
func PathExists(path string) (bool, error) {
|
||||||
_, err := os.Stat(path)
|
_, err := os.Stat(path)
|
||||||
@ -12,3 +34,60 @@ func PathExists(path string) (bool, error) {
|
|||||||
}
|
}
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Mkdir(path string) error {
|
||||||
|
return os.MkdirAll(path, os.ModePerm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UploadImage(r io.Reader) (string, error) {
|
||||||
|
img, format, err := image.Decode(r)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("读取图片错误")
|
||||||
|
}
|
||||||
|
if errors.Is(err, image.ErrFormat) {
|
||||||
|
return "", ErrUnsupportedFileFormat
|
||||||
|
}
|
||||||
|
if format != "png" && format != "jpeg" && format != "gif" {
|
||||||
|
return "", ErrUnsupportedFileFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存放目录
|
||||||
|
curTime := time.Now()
|
||||||
|
dir := path.Join("upload", "imgs", curTime.Format("2006"), curTime.Format("01"), curTime.Format("02"))
|
||||||
|
exist, _ := PathExists(dir)
|
||||||
|
if !exist {
|
||||||
|
err := Mkdir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return "", ErrFileStorePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filename, err := nanoId.Nanoid()
|
||||||
|
if err != nil {
|
||||||
|
return "", ErrFileGenerateName
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := path.Join("", dir, filename+"."+format)
|
||||||
|
f, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", ErrFileStorePath
|
||||||
|
}
|
||||||
|
defer func(f *os.File) {
|
||||||
|
_ = f.Close()
|
||||||
|
}(f)
|
||||||
|
|
||||||
|
var e error
|
||||||
|
switch format {
|
||||||
|
case "png":
|
||||||
|
e = png.Encode(f, img)
|
||||||
|
case "jpeg":
|
||||||
|
e = jpeg.Encode(f, img, nil)
|
||||||
|
case "gif":
|
||||||
|
e = gif.Encode(f, img, nil)
|
||||||
|
}
|
||||||
|
if e != nil {
|
||||||
|
return "", ErrFileSaveFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
return "/" + filePath, nil
|
||||||
|
}
|
||||||
|
|||||||
4
main.go
4
main.go
@ -11,6 +11,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// filename, _ := nanoId.Nanoid()
|
||||||
|
// log.Println(filename)
|
||||||
|
// return
|
||||||
|
|
||||||
config, err := config.LoadConfig(".")
|
config, err := config.LoadConfig(".")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("cannot load config: ", err)
|
log.Fatal("cannot load config: ", err)
|
||||||
|
|||||||
@ -1,5 +1,11 @@
|
|||||||
@charset "utf-8";
|
@charset "utf-8";
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
/* 禁用空格键的滚动 */
|
||||||
|
scroll-behavior: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.flex {
|
.flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
@ -22,7 +28,7 @@
|
|||||||
|
|
||||||
.navbar-wh {
|
.navbar-wh {
|
||||||
padding: 0.2rem 1.6rem;
|
padding: 0.2rem 1.6rem;
|
||||||
border-bottom: 1px solid rgba(0,0,0,.12);
|
border-bottom: 1px solid rgba(0, 0, 0, .12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-brand-fs {
|
.navbar-brand-fs {
|
||||||
@ -54,6 +60,17 @@
|
|||||||
padding-bottom: 60px;
|
padding-bottom: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main .title{
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main .title a{
|
||||||
|
height: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
.tip-box {
|
.tip-box {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: rgb(229, 246, 253);
|
background-color: rgb(229, 246, 253);
|
||||||
@ -68,7 +85,7 @@
|
|||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tip-box .tip-icon svg{
|
.tip-box .tip-icon svg {
|
||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
color: #007bff;
|
color: #007bff;
|
||||||
}
|
}
|
||||||
@ -77,7 +94,18 @@
|
|||||||
letter-spacing: 0.00938em;
|
letter-spacing: 0.00938em;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color:rgb(1, 67, 97);
|
color: rgb(1, 67, 97);
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-list {
|
||||||
|
display: grid;
|
||||||
|
/* 声明列的宽度 */
|
||||||
|
grid-template-columns: repeat(4, 305px);
|
||||||
|
/* 声明行间距和列间距 */
|
||||||
|
grid-gap: 20px;
|
||||||
|
/* 声明行的高度 */
|
||||||
|
/* grid-template-rows: 360px; */
|
||||||
|
margin-top: 25px;
|
||||||
}
|
}
|
||||||
2
web/statics/js/jquery.min.js
vendored
Normal file
2
web/statics/js/jquery.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -11,7 +11,7 @@
|
|||||||
<div class="wrapper"> -->
|
<div class="wrapper"> -->
|
||||||
{{define "footer"}}
|
{{define "footer"}}
|
||||||
</div>
|
</div>
|
||||||
<script src="/statics/js/jquery.slim.min.js"></script>
|
<script src="/statics/js/jquery.min.js"></script>
|
||||||
<script src="/statics/js/bootstrap.bundle.min.js"></script>
|
<script src="/statics/js/bootstrap.bundle.min.js"></script>
|
||||||
{{block "js" .}}{{end}}
|
{{block "js" .}}{{end}}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -21,11 +21,14 @@
|
|||||||
HLS流媒体
|
HLS流媒体
|
||||||
</a>
|
</a>
|
||||||
<ul class="flex oauth">
|
<ul class="flex oauth">
|
||||||
{{if .AuthID}}
|
{{if .Authorize.ID}}
|
||||||
<li>
|
<li style="font-size: 12px;">
|
||||||
欢迎您: {{.AuthName}}
|
欢迎您: {{.Authorize.Name}}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li style="font-size: 12px;">
|
||||||
|
<a href="/me/videos">我的视频</a>
|
||||||
|
</li>
|
||||||
|
<li style="font-size: 12px;">
|
||||||
<a href="/logout" class="btn btn-primary btn-sm">退出</a>
|
<a href="/logout" class="btn btn-primary btn-sm">退出</a>
|
||||||
</li>
|
</li>
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|||||||
@ -1,51 +1,23 @@
|
|||||||
<!-- <!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>首页 - HLS流视频</title>
|
|
||||||
<style>
|
|
||||||
.container{
|
|
||||||
width: 640px;
|
|
||||||
}
|
|
||||||
.container #video{
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<video id="video" controls></video>
|
|
||||||
</div>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
|
|
||||||
<script>
|
|
||||||
var video = document.getElementById('video');
|
|
||||||
if(Hls.isSupported()) {
|
|
||||||
console.log("supported");
|
|
||||||
var hls = new Hls();
|
|
||||||
hls.loadSource('http://192.168.9.252:9090/statics/clip7iilsbb2u3en7mt0/index.m3u8');
|
|
||||||
hls.attachMedia(video);
|
|
||||||
hls.on(Hls.Events.MANIFEST_PARSED,function() {
|
|
||||||
video.play();
|
|
||||||
});
|
|
||||||
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
|
||||||
console.log("no supported");
|
|
||||||
video.src = 'http://192.168.9.252:9090/stream';
|
|
||||||
video.addEventListener('loadedmetadata',function() {
|
|
||||||
video.play();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html> -->
|
|
||||||
|
|
||||||
{{template "header" .}}
|
{{template "header" .}}
|
||||||
<div class="container-fluid flex justify-content">
|
<div class="container-fluid flex justify-content">
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<h3 style="margin-top: 20px;margin-bottom: 10px;">视频列表</h3>
|
<h3 style="margin-top: 20px;margin-bottom: 10px;">视频列表</h3>
|
||||||
|
<div class="video-list">
|
||||||
|
{{range .Videos}}
|
||||||
|
<div class="card">
|
||||||
|
<img src="{{.Images}}" class="card-img-top" alt="{{.Title}}">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{{.Title}}</h5>
|
||||||
|
<p class="card-text">{{.Description}}</p>
|
||||||
|
<a href="/play/{{.ID}}" class="btn btn-primary">播放</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{define "js"}}
|
||||||
|
<script>
|
||||||
|
</script>
|
||||||
|
{{end}}
|
||||||
{{template "footer" .}}
|
{{template "footer" .}}
|
||||||
73
web/templates/me/videos.html.tmpl
Normal file
73
web/templates/me/videos.html.tmpl
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
{{template "header" .}}
|
||||||
|
<div class="container-fluid flex justify-content">
|
||||||
|
<div class="main">
|
||||||
|
<div class="title">
|
||||||
|
<h3>我的视频</h3>
|
||||||
|
<a href="/me/videos/create" class="btn btn-primary">添加</a>
|
||||||
|
</div>
|
||||||
|
<div class="video-list">
|
||||||
|
{{range .Videos}}
|
||||||
|
<div class="card">
|
||||||
|
<img src="{{.Images}}" class="card-img-top" alt="{{.Title}}">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">{{.Title}}</h5>
|
||||||
|
<p class="card-text">{{.Description}}</p>
|
||||||
|
|
||||||
|
<a href="/me/videos/create/{{.ID}}" class="btn btn-warning">编辑</a>
|
||||||
|
<button id="del" data-id="{{.ID}}" class="btn btn-danger">删除</button>
|
||||||
|
|
||||||
|
{{if eq .Status 0}}
|
||||||
|
<button id="transfer" data-id="{{.ID}}" class="btn btn-info">转码</button>
|
||||||
|
{{else if eq .Status 200}}
|
||||||
|
<a href="/play/{{.ID}}" class="btn btn-primary">播放</a>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{{if eq .Status 1}}
|
||||||
|
<p>转码中...</p>
|
||||||
|
{{else if eq .Status 2}}
|
||||||
|
<p>转码失败</p>
|
||||||
|
{{end}}
|
||||||
|
<p id="msg"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{define "js"}}
|
||||||
|
<script>
|
||||||
|
$('#transfer').click(function () {
|
||||||
|
let that = $(this)
|
||||||
|
that.attr("disable", true).html('转码中...')
|
||||||
|
let id = that.attr("data-id")
|
||||||
|
$.ajax({
|
||||||
|
url: '/transfer/' + id,
|
||||||
|
type: 'post',
|
||||||
|
success: function (obj) {
|
||||||
|
$('#msg').html(obj)
|
||||||
|
},
|
||||||
|
error: function (ex) {
|
||||||
|
console.log(ex)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
$('#del').click(function () {
|
||||||
|
let that = $(this)
|
||||||
|
that.attr("disable", true).html('删除中...')
|
||||||
|
let id = that.attr("data-id")
|
||||||
|
$.ajax({
|
||||||
|
url: '/transfer/' + id,
|
||||||
|
type: 'post',
|
||||||
|
success: function (obj) {
|
||||||
|
$('#msg').html(obj)
|
||||||
|
},
|
||||||
|
error: function (ex) {
|
||||||
|
console.log(ex)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{{end}}
|
||||||
|
{{template "footer" .}}
|
||||||
@ -1,31 +1,41 @@
|
|||||||
{{template "header" .}}
|
{{template "header" .}}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="flex flex-column align-items row py-md-5">
|
<div class="flex flex-column align-items row py-md-5 mt-md-5">
|
||||||
<h1>登录</h1>
|
<h1>登录</h1>
|
||||||
<div class="col-sm-5 py-md-5">
|
<div class="col-sm-4 py-md-5">
|
||||||
<form action="/login" method="post">
|
<form action="/login" method="post">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="email">邮箱</label>
|
<div class="input-group">
|
||||||
<input type="email" name="email" class="form-control" required id="email" value="{{.Email}}" aria-describedby="emailValid">
|
<div class="input-group-prepend">
|
||||||
{{if .EmailErr}}
|
<span class="input-group-text">邮箱</span>
|
||||||
<small id="emailValid" style="color: #f44336;" class="form-text">{{.EmailErr}}</small>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<input type="email" name="email" class="form-control" required id="email" value="{{.Email}}"
|
||||||
<label for="password">密码</label>
|
aria-describedby="emailValid">
|
||||||
<input type="password" name="password" class="form-control" required id="password" value="{{.Password}}" aria-describedby="passwordValid">
|
</div>
|
||||||
{{if .PasswordErr}}
|
{{if .EmailErr}}
|
||||||
<small id="passwordValid" style="color: #f44336;" class="form-text">{{.PasswordErr}}</small>
|
<small id="emailValid" style="color: #f44336;" class="form-text">{{.EmailErr}}</small>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary">提交</button>
|
|
||||||
</form>
|
|
||||||
{{if .Summary}}
|
|
||||||
<div class="py-md-5" style="color: #f44336;">
|
|
||||||
{{.Summary}}
|
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
<div class="form-group">
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">密码</span>
|
||||||
|
</div>
|
||||||
|
<input type="password" name="password" class="form-control" required id="password" value="{{.Password}}"
|
||||||
|
aria-describedby="passwordValid">
|
||||||
|
</div>
|
||||||
|
{{if .PasswordErr}}
|
||||||
|
<small id="passwordValid" style="color: #f44336;" class="form-text">{{.PasswordErr}}</small>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary btn-block">提交</button>
|
||||||
|
</form>
|
||||||
|
{{if .Summary}}
|
||||||
|
<div class="py-md-5" style="color: #f44336;">
|
||||||
|
{{.Summary}}
|
||||||
</div>
|
</div>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{{template "footer" .}}
|
{{template "footer" .}}
|
||||||
@ -1,38 +1,53 @@
|
|||||||
{{template "header" .}}
|
{{template "header" .}}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="flex flex-column align-items row py-md-5">
|
<div class="flex flex-column align-items row py-md-5 mt-md-5">
|
||||||
<h1>注册</h1>
|
<h1>注册</h1>
|
||||||
<div class="col-sm-5 py-md-5">
|
<div class="col-sm-4 py-md-5">
|
||||||
<form action="/register" method="post">
|
<form action="/register" method="post">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="email">邮箱</label>
|
<div class="input-group">
|
||||||
<input type="email" name="email" class="form-control" required id="email" value="{{.Email}}" aria-describedby="emailValid">
|
<div class="input-group-prepend">
|
||||||
{{if .EmailErr}}
|
<span class="input-group-text">邮箱</span>
|
||||||
<small id="emailValid" style="color: #f44336;" class="form-text">{{.EmailErr}}</small>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<input type="email" name="email" class="form-control" required id="email" value="{{.Email}}"
|
||||||
<label for="username">名称</label>
|
aria-describedby="emailValid">
|
||||||
<input type="text" name="username" class="form-control" required id="username" value="{{.Username}}" style="width: 100%;" aria-describedby="usernameValid">
|
</div>
|
||||||
{{if .UsernameErr}}
|
{{if .EmailErr}}
|
||||||
<small id="usernameValid" style="color: #f44336;" class="form-text">{{.UsernameErr}}</small>
|
<small id="emailValid" style="color: #f44336;" class="form-text">{{.EmailErr}}</small>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="password">密码</label>
|
|
||||||
<input type="password" name="password" class="form-control" required id="password" value="{{.Password}}" aria-describedby="passwordValid">
|
|
||||||
{{if .PasswordErr}}
|
|
||||||
<small id="passwordValid" style="color: #f44336;" class="form-text">{{.PasswordErr}}</small>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary">提交</button>
|
|
||||||
</form>
|
|
||||||
{{if .Summary}}
|
|
||||||
<div class="py-md-5" style="color: #f44336;">
|
|
||||||
{{.Summary}}
|
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
<div class="form-group">
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">名称</span>
|
||||||
|
</div>
|
||||||
|
<input type="text" name="username" class="form-control" required id="username" value="{{.Username}}"
|
||||||
|
aria-describedby="usernameValid">
|
||||||
|
</div>
|
||||||
|
{{if .UsernameErr}}
|
||||||
|
<small id="usernameValid" style="color: #f44336;" class="form-text">{{.UsernameErr}}</small>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">密码</span>
|
||||||
|
</div>
|
||||||
|
<input type="password" name="password" class="form-control" required id="password" value="{{.Password}}"
|
||||||
|
aria-describedby="passwordValid">
|
||||||
|
</div>
|
||||||
|
{{if .PasswordErr}}
|
||||||
|
<small id="passwordValid" style="color: #f44336;" class="form-text">{{.PasswordErr}}</small>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary btn-block">提交</button>
|
||||||
|
</form>
|
||||||
|
{{if .Summary}}
|
||||||
|
<div class="py-md-5" style="color: #f44336;">
|
||||||
|
{{.Summary}}
|
||||||
</div>
|
</div>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{{template "footer" .}}
|
{{template "footer" .}}
|
||||||
126
web/templates/video/edit.html.tmpl
Normal file
126
web/templates/video/edit.html.tmpl
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
{{template "header" .}}
|
||||||
|
<div class="container-fluid flex justify-content">
|
||||||
|
<div class="main">
|
||||||
|
<div class="title">
|
||||||
|
<h3>上传视频</h3>
|
||||||
|
<a href="/me/videos" class="btn btn-primary">返回列表</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6 py-md-5 flex flex-column justify-content">
|
||||||
|
<form action="/me/videos/create" method="post">
|
||||||
|
{{if .ID}}
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">ID</span>
|
||||||
|
</div>
|
||||||
|
<input type="text" class="form-control" disabled value="{{.ID}}">
|
||||||
|
<input type="hidden" value="{{.ID}}" name="id">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">标题</span>
|
||||||
|
</div>
|
||||||
|
<input type="text" name="title" class="form-control" required id="title" value="{{.Title}}"
|
||||||
|
aria-describedby="titleValid">
|
||||||
|
</div>
|
||||||
|
{{if .TitleErr}}
|
||||||
|
<small id="titleValid" style="color: #f44336;" class="form-text">{{.TitleErr}}</small>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">图片</span>
|
||||||
|
</div>
|
||||||
|
<input type="file" class="form-control" id="upload_images">
|
||||||
|
<input type="button" class="btn btn-secondary" value="上传" onclick="uploadImage()" />
|
||||||
|
<input type="hidden" id="images" name="images" required value="{{.Images}}" aria-describedby="imagesValid">
|
||||||
|
</div>
|
||||||
|
<div class="image-box" style="margin-top: 8px;">
|
||||||
|
{{if .Images}}
|
||||||
|
<img width="120px" src="{{.Images}}" />
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{if .ImagesErr}}
|
||||||
|
<small id="imagesValid" style="color: #f44336;" class="form-text">{{.ImagesErr}}</small>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">描述</span>
|
||||||
|
</div>
|
||||||
|
<textarea class="form-control" id="description" name="description" required
|
||||||
|
aria-describedby="descriptionValid" rows="3">{{.Description}}</textarea>
|
||||||
|
</div>
|
||||||
|
{{if .DescriptionErr}}
|
||||||
|
<small id="descriptionValid" style="color: #f44336;" class="form-text">{{.DescriptionErr}}</small>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">视频</span>
|
||||||
|
</div>
|
||||||
|
<input type="file" class="form-control" id="upload_video">
|
||||||
|
<input type="button" class="btn btn-secondary" value="上传" onclick="uploadVideo()" />
|
||||||
|
<input type="hidden" id="video" name="origin_link" required value="{{.OriginLink}}">
|
||||||
|
</div>
|
||||||
|
<div class="video-box" style="margin-top: 8px;">
|
||||||
|
{{if .OriginLink}}
|
||||||
|
<p>{{.OriginLink}}</p>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<small id="upload_video_msg" style="color: #f44336;" class="form-text">
|
||||||
|
{{.OriginLinkErr}}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">提交</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{define "js"}}
|
||||||
|
<script>
|
||||||
|
function uploadImage() {
|
||||||
|
var files = $('#upload_images').prop('files');
|
||||||
|
var data = new FormData();
|
||||||
|
data.append('file', files[0]);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/upload_image',
|
||||||
|
type: 'POST',
|
||||||
|
data: data,
|
||||||
|
cache: false,
|
||||||
|
processData: false,
|
||||||
|
contentType: false,
|
||||||
|
success: function (res) {
|
||||||
|
$('#images').val(res);
|
||||||
|
$('.image-box').append('<img width="120px" src="' + res + '" />');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function uploadVideo() {
|
||||||
|
var files = $('#upload_video').prop('files');
|
||||||
|
var data = new FormData();
|
||||||
|
data.append('file', files[0]);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/upload_file',
|
||||||
|
type: 'POST',
|
||||||
|
data: data,
|
||||||
|
cache: false,
|
||||||
|
processData: false,
|
||||||
|
contentType: false,
|
||||||
|
success: function (res) {
|
||||||
|
$('#video').val(res);
|
||||||
|
$('#upload_video_msg').html('上传成功')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{{end}}
|
||||||
|
{{template "footer" .}}
|
||||||
@ -1,14 +1,25 @@
|
|||||||
{{template "header" .}}
|
{{template "header" .}}
|
||||||
<div class="container-fluid flex justify-content">
|
<div class="container-fluid flex justify-content">
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<h6 style="margin-top: 20px;margin-bottom: 10px;">正在播放
|
{{if eq .Video.Status 200}}
|
||||||
|
<h6 style="margin-top: 20px;margin-bottom: 10px;">正在播放
|
||||||
<svg class="icon" viewBox="0 0 1024 1024" width="15" height="21">
|
<svg class="icon" viewBox="0 0 1024 1024" width="15" height="21">
|
||||||
<path d="M607.698947 992.000352c0-7.399919-2.599971-14.899836-7.799914-20.89977l-360.79603-416.99541c-20.999769-23.999736-20.999769-60.299336 0-84.299073l0.099999-0.099999L599.899033 52.910688c11.599872-13.399853 10.099889-33.59963-3.299964-45.099504-13.399853-11.599872-33.59963-10.099889-45.099504 3.299964L190.903534 427.806562c-20.399775 23.299744-31.599652 53.199414-31.599652 84.199073s11.199877 60.89933 31.599652 84.199073l360.596031 416.695414c11.599872 13.399853 31.79965 14.799837 45.099504 3.299964 7.399919-6.299931 11.099878-15.199833 11.099878-24.199734z" p-id="6425"></path><path d="M864.696118 992.000352c0-7.399919-2.599971-14.899836-7.799914-20.89977l-360.796029-416.99541c-20.999769-23.999736-20.999769-60.299336 0-84.299073l0.099999-0.099999L856.896204 52.910688c11.599872-13.399853 10.099889-33.59963-3.299964-45.099504-13.399853-11.599872-33.59963-10.099889-45.099503 3.299964L447.900705 427.806562c-20.399775 23.299744-31.599652 53.199414-31.599652 84.199073s11.199877 60.89933 31.599652 84.199073l360.596032 416.695414c11.599872 13.399853 31.79965 14.799837 45.099503 3.299964 7.399919-6.299931 11.099878-15.199833 11.099878-24.199734z" p-id="6426"></path>
|
<path
|
||||||
</svg>Git - Div Rhino<svg class="icon" viewBox="0 0 1024 1024" width="15" height="21">
|
d="M607.698947 992.000352c0-7.399919-2.599971-14.899836-7.799914-20.89977l-360.79603-416.99541c-20.999769-23.999736-20.999769-60.299336 0-84.299073l0.099999-0.099999L599.899033 52.910688c11.599872-13.399853 10.099889-33.59963-3.299964-45.099504-13.399853-11.599872-33.59963-10.099889-45.099504 3.299964L190.903534 427.806562c-20.399775 23.299744-31.599652 53.199414-31.599652 84.199073s11.199877 60.89933 31.599652 84.199073l360.596031 416.695414c11.599872 13.399853 31.79965 14.799837 45.099504 3.299964 7.399919-6.299931 11.099878-15.199833 11.099878-24.199734z"
|
||||||
<path d="M416.301053 992.000352c0-7.399919 2.599971-14.899836 7.799914-20.89977l360.79603-416.895412c20.999769-23.999736 20.999769-60.299336 0-84.299072l-0.099999-0.099999L424.100967 52.910688c-11.599872-13.399853-10.099889-33.59963 3.299964-45.099504 13.399853-11.599872 33.59963-10.099889 45.099504 3.299964l360.596031 416.695414c20.399775 23.299744 31.599652 53.199414 31.599652 84.199073s-11.199877 60.89933-31.599652 84.199073l-360.596031 416.695414c-11.599872 13.399853-31.79965 14.799837-45.099504 3.299964-7.399919-6.299931-11.099878-15.199833-11.099878-24.199734z" p-id="6274"></path><path d="M159.303882 992.000352c0-7.399919 2.599971-14.899836 7.799914-20.89977l360.796029-416.895412c20.999769-23.999736 20.999769-60.299336 0-84.299072l-0.099999-0.099999L167.103796 52.910688c-11.599872-13.399853-10.099889-33.59963 3.299964-45.099504 13.399853-11.599872 33.59963-10.099889 45.099503 3.299964l360.596032 416.695414c20.399775 23.299744 31.599652 53.199414 31.599652 84.199073s-11.199877 60.89933-31.599652 84.199073l-360.596032 416.695414c-11.599872 13.399853-31.79965 14.799837-45.099503 3.299964-7.399919-6.299931-11.099878-15.199833-11.099878-24.199734z" p-id="6275"></path>
|
p-id="6425"></path>
|
||||||
|
<path
|
||||||
|
d="M864.696118 992.000352c0-7.399919-2.599971-14.899836-7.799914-20.89977l-360.796029-416.99541c-20.999769-23.999736-20.999769-60.299336 0-84.299073l0.099999-0.099999L856.896204 52.910688c11.599872-13.399853 10.099889-33.59963-3.299964-45.099504-13.399853-11.599872-33.59963-10.099889-45.099503 3.299964L447.900705 427.806562c-20.399775 23.299744-31.599652 53.199414-31.599652 84.199073s11.199877 60.89933 31.599652 84.199073l360.596032 416.695414c11.599872 13.399853 31.79965 14.799837 45.099503 3.299964 7.399919-6.299931 11.099878-15.199833 11.099878-24.199734z"
|
||||||
|
p-id="6426"></path>
|
||||||
|
</svg>{{.Video.Title}}<svg class="icon" viewBox="0 0 1024 1024" width="15" height="21">
|
||||||
|
<path
|
||||||
|
d="M416.301053 992.000352c0-7.399919 2.599971-14.899836 7.799914-20.89977l360.79603-416.895412c20.999769-23.999736 20.999769-60.299336 0-84.299072l-0.099999-0.099999L424.100967 52.910688c-11.599872-13.399853-10.099889-33.59963 3.299964-45.099504 13.399853-11.599872 33.59963-10.099889 45.099504 3.299964l360.596031 416.695414c20.399775 23.299744 31.599652 53.199414 31.599652 84.199073s-11.199877 60.89933-31.599652 84.199073l-360.596031 416.695414c-11.599872 13.399853-31.79965 14.799837-45.099504 3.299964-7.399919-6.299931-11.099878-15.199833-11.099878-24.199734z"
|
||||||
|
p-id="6274"></path>
|
||||||
|
<path
|
||||||
|
d="M159.303882 992.000352c0-7.399919 2.599971-14.899836 7.799914-20.89977l360.796029-416.895412c20.999769-23.999736 20.999769-60.299336 0-84.299072l-0.099999-0.099999L167.103796 52.910688c-11.599872-13.399853-10.099889-33.59963 3.299964-45.099504 13.399853-11.599872 33.59963-10.099889 45.099503 3.299964l360.596032 416.695414c20.399775 23.299744 31.599652 53.199414 31.599652 84.199073s-11.199877 60.89933-31.599652 84.199073l-360.596032 416.695414c-11.599872 13.399853-31.79965 14.799837-45.099503 3.299964-7.399919-6.299931-11.099878-15.199833-11.099878-24.199734z"
|
||||||
|
p-id="6275"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</h6>
|
</h6>
|
||||||
<video id="video" controls width="100%"></video>
|
<video id="video" controls width="100%" style="background-color: #000000;"></video>
|
||||||
<div class="tip-box">
|
<div class="tip-box">
|
||||||
<div class="tip-icon">
|
<div class="tip-icon">
|
||||||
<svg focusable="false" aria-hidden="true" viewBox="0 0 24 24" data-testid="InfoOutlinedIcon">
|
<svg focusable="false" aria-hidden="true" viewBox="0 0 24 24" data-testid="InfoOutlinedIcon">
|
||||||
@ -22,22 +33,27 @@
|
|||||||
<p>如果播放遇到问题,请尝试切换播放源。</p>
|
<p>如果播放遇到问题,请尝试切换播放源。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="width: 100%; height: 600px;background-color: antiquewhite;"></div>
|
||||||
|
{{else}}
|
||||||
|
<div class="">该视频目前还不能播放!</div>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{define "js"}}
|
||||||
<script src="/statics/js/hls.min.js"></script>
|
<script src="/statics/js/hls.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
var video = document.getElementById('video');
|
var video = document.getElementById('video');
|
||||||
if (Hls.isSupported()) {
|
if (Hls.isSupported()) {
|
||||||
console.log("supported");
|
console.log("supported");
|
||||||
var hls = new Hls();
|
var hls = new Hls();
|
||||||
hls.loadSource('{{.Url}}');
|
hls.loadSource('{{.Video.PlayLink}}');
|
||||||
hls.attachMedia(video);
|
hls.attachMedia(video);
|
||||||
hls.on(Hls.Events.MANIFEST_PARSED, function () {
|
hls.on(Hls.Events.MANIFEST_PARSED, function () {
|
||||||
//video.play();
|
//video.play();
|
||||||
});
|
});
|
||||||
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||||
console.log("no supported");
|
console.log("no supported");
|
||||||
video.src = '{{.Url}}';
|
video.src = '{{.Video.PlayLink}}';
|
||||||
video.addEventListener('loadedmetadata', function () {
|
video.addEventListener('loadedmetadata', function () {
|
||||||
//video.play();
|
//video.play();
|
||||||
});
|
});
|
||||||
@ -45,7 +61,10 @@
|
|||||||
// reurn false 禁止函数内部执行其他的事件或者方法
|
// reurn false 禁止函数内部执行其他的事件或者方法
|
||||||
var vol = 0.1; // 1代表100%音量,每次增减0.1
|
var vol = 0.1; // 1代表100%音量,每次增减0.1
|
||||||
var time = 10; // 单位秒,每次增减10秒
|
var time = 10; // 单位秒,每次增减10秒
|
||||||
document.onkeyup = function (event) {//键盘事件
|
// 键盘事件
|
||||||
|
document.onkeyup = function (event) {
|
||||||
|
const nodeName = event.target.nodeName;
|
||||||
|
if (nodeName && (nodeName.toUpperCase() === "INPUT" || nodeName.toUpperCase() === "TEXTAREA")) { return; }
|
||||||
// console.log("keyCode:" + event.keyCode);
|
// console.log("keyCode:" + event.keyCode);
|
||||||
var e = event || window.event || arguments.callee.caller.arguments[0];
|
var e = event || window.event || arguments.callee.caller.arguments[0];
|
||||||
// 鼠标上下键控制视频音量
|
// 鼠标上下键控制视频音量
|
||||||
@ -66,10 +85,19 @@
|
|||||||
video.volume !== video.duration ? video.currentTime += time : 1;
|
video.volume !== video.duration ? video.currentTime += time : 1;
|
||||||
return false;
|
return false;
|
||||||
} else if (e && e.keyCode === 32) {
|
} else if (e && e.keyCode === 32) {
|
||||||
|
// if (e.preventDefault) {
|
||||||
|
// e.preventDefault();
|
||||||
|
// } else {
|
||||||
|
// window.event.returnValue = false;
|
||||||
|
// }
|
||||||
|
// event.preventDefault();
|
||||||
|
// event.returnValue = false;
|
||||||
// 按空格键 判断当前是否暂停
|
// 按空格键 判断当前是否暂停
|
||||||
|
// alert(1)
|
||||||
video.paused === true ? video.play() : video.pause();
|
video.paused === true ? video.play() : video.pause();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
{{end}}
|
||||||
{{template "footer" .}}
|
{{template "footer" .}}
|
||||||
43
web/templates/video/transfer.html.tmpl
Normal file
43
web/templates/video/transfer.html.tmpl
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
{{template "header" .}}
|
||||||
|
<div class="container-fluid flex justify-content">
|
||||||
|
<div class="main">
|
||||||
|
<div class="title">
|
||||||
|
<h3>视频转码</h3>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5>{{.Video.Title}}</h5>
|
||||||
|
<p>{{.Video.Description}}</p>
|
||||||
|
<p><img src="{{.Video.Images}}" width="120px" /></p>
|
||||||
|
{{if eq .Video.Status 0}}
|
||||||
|
<p>该视频需要转码才能播放</p>
|
||||||
|
<button id="transfer" class="btn btn-primary">转码</button>
|
||||||
|
{{else if eq .Video.Status 1}}
|
||||||
|
<p>转码中...</p>
|
||||||
|
{{else if eq .Video.Status 2}}
|
||||||
|
<p>转码失败, 请联系管理员</p>
|
||||||
|
{{else if eq .Video.Status 200}}
|
||||||
|
<p>恭喜您, 转码成功!</p>
|
||||||
|
{{end}}
|
||||||
|
<p id="msg"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{define "js"}}
|
||||||
|
<script>
|
||||||
|
$('#transfer').click(function () {
|
||||||
|
let that = $(this)
|
||||||
|
that.attr("disable", true).html('转码中...')
|
||||||
|
$.ajax({
|
||||||
|
url: '/transfer/{{.Video.ID}}',
|
||||||
|
type: 'post',
|
||||||
|
success: function (obj) {
|
||||||
|
$('#msg').html(obj)
|
||||||
|
},
|
||||||
|
error: function (ex) {
|
||||||
|
console.log(ex)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{{end}}
|
||||||
|
{{template "footer" .}}
|
||||||
Loading…
x
Reference in New Issue
Block a user