diff --git a/backend/__debug_bin1422413187 b/backend/__debug_bin1540611960 similarity index 80% rename from backend/__debug_bin1422413187 rename to backend/__debug_bin1540611960 index 551bf80..1701e60 100755 Binary files a/backend/__debug_bin1422413187 and b/backend/__debug_bin1540611960 differ diff --git a/backend/go.mod b/backend/go.mod index 5719915..9cc0a3f 100755 --- a/backend/go.mod +++ b/backend/go.mod @@ -8,7 +8,7 @@ require ( github.com/gofiber/fiber/v3 v3.0.0-beta.3 github.com/gofrs/uuid v4.4.0+incompatible github.com/golang-jwt/jwt/v4 v4.5.1 - github.com/google/uuid v1.6.0 + github.com/grafov/m3u8 v0.12.1 github.com/imroc/req/v3 v3.48.0 github.com/jinzhu/copier v0.4.0 github.com/juju/go4 v0.0.0-20160222163258-40d72ab9641a @@ -38,6 +38,7 @@ require ( github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gofiber/utils/v2 v2.0.0-beta.4 // indirect github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect diff --git a/backend/go.sum b/backend/go.sum index 854630c..0ff49ed 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -159,6 +159,8 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/grafov/m3u8 v0.12.1 h1:DuP1uA1kvRRmGNAZ0m+ObLv1dvrfNO0TPx0c/enNk0s= +github.com/grafov/m3u8 v0.12.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= diff --git a/backend/main.go b/backend/main.go index 7563e09..abb3c3f 100755 --- a/backend/main.go +++ b/backend/main.go @@ -4,11 +4,11 @@ package main import ( - "backend/common/service/http" - "backend/common/service/migrate" - "backend/common/service/model" - "backend/common/service/tasks" - "backend/common/service/tenants" + "backend/pkg/service/http" + "backend/pkg/service/migrate" + "backend/pkg/service/model" + "backend/pkg/service/tasks" + "backend/pkg/service/tenants" "git.ipao.vip/rogeecn/atom" log "github.com/sirupsen/logrus" diff --git a/backend/main_test.go b/backend/main_test.go index 3532d3a..7ddd583 100755 --- a/backend/main_test.go +++ b/backend/main_test.go @@ -4,8 +4,8 @@ import ( "encoding/json" "testing" - "backend/common/service/model" "backend/pkg/pg" + "backend/pkg/service/model" "backend/providers/wechat" "git.ipao.vip/rogeecn/atom" diff --git a/backend/modules/commands/discover/discover_medias.go b/backend/modules/commands/discover/discover_medias.go index 892723e..82ba5ee 100644 --- a/backend/modules/commands/discover/discover_medias.go +++ b/backend/modules/commands/discover/discover_medias.go @@ -12,8 +12,8 @@ import ( "strconv" "strings" - "backend/common/media_store" "backend/modules/medias" + "backend/pkg/media_store" "backend/pkg/path" "github.com/pkg/errors" diff --git a/backend/modules/commands/discover/discover_medias_test.go b/backend/modules/commands/discover/discover_medias_test.go index 42aa4eb..39e44ad 100644 --- a/backend/modules/commands/discover/discover_medias_test.go +++ b/backend/modules/commands/discover/discover_medias_test.go @@ -4,9 +4,9 @@ import ( "os" "testing" - "backend/common/media_store" - "backend/common/service/testx" "backend/modules/medias" + "backend/pkg/media_store" + "backend/pkg/service/testx" "backend/providers/postgres" . "github.com/smartystreets/goconvey/convey" diff --git a/backend/modules/commands/store/store_medias.go b/backend/modules/commands/store/store_medias.go index f90378a..99bfc0b 100644 --- a/backend/modules/commands/store/store_medias.go +++ b/backend/modules/commands/store/store_medias.go @@ -3,8 +3,8 @@ package store import ( "context" - "backend/common/media_store" "backend/modules/medias" + "backend/pkg/media_store" "github.com/pkg/errors" log "github.com/sirupsen/logrus" diff --git a/backend/modules/medias/controller.go b/backend/modules/medias/controller.go index d7a04df..b2f31f4 100644 --- a/backend/modules/medias/controller.go +++ b/backend/modules/medias/controller.go @@ -1,17 +1,20 @@ package medias import ( - "backend/common/consts" - "backend/common/errorx" + "backend/pkg/consts" + "backend/pkg/errorx" + "backend/pkg/pg" "backend/providers/jwt" "github.com/gofiber/fiber/v3" log "github.com/sirupsen/logrus" + hashids "github.com/speps/go-hashids/v2" ) // @provider type Controller struct { - svc *Service + hashIds *hashids.HashID + svc *Service } // List @@ -54,10 +57,60 @@ func (c *Controller) Show(ctx fiber.Ctx) error { } // Audio -func (c *Controller) Video(ctx fiber.Ctx) error { - // mediaId := ToInt64(ctx.Params("media")) - // tenantId := ToInt64(ctx.Locals("tenantId")) - // userId := ToInt64(ctx.Locals("userId")) +func (c *Controller) MediaIndex(ctx fiber.Ctx) error { + mediaType, err := pg.ParseMediaType(ctx.Params("type")) + if err != nil { + return ctx.Status(fiber.StatusInternalServerError).JSON(errorx.RequestParseError) + } - return ctx.JSON(nil) + hash := ctx.Params("hash") + claim := fiber.Locals[*jwt.Claims](ctx, consts.CtxKeyClaim) + log.Debug(claim) + + model, err := c.svc.GetMediaByHash(ctx.Context(), claim.TenantID, hash) + if err != nil { + log.WithField("action", "medias.MediaIndex").WithError(err).Error("GetMediaByHash") + return ctx.Status(fiber.StatusInternalServerError).JSON(errorx.InternalError) + } + + bought, err := c.svc.HasUserBought(ctx.Context(), claim.TenantID, claim.UserID, model.ID) + if err != nil { + log.WithField("action", "medias.MediaIndex").WithError(err).Error("HasUserBought") + return ctx.Status(fiber.StatusInternalServerError).JSON(errorx.InternalError) + } + + playlist, err := c.svc.GetM3U8(ctx.Context(), claim.TenantID, mediaType, model.Hash, bought) + if err != nil { + log.WithField("action", "medias.MediaIndex").WithError(err).Error("GetMediaPlaylist") + return ctx.Status(fiber.StatusInternalServerError).JSON(errorx.InternalError) + } + + return ctx.SendString(playlist.String()) +} + +func (c *Controller) MediaSegment(ctx fiber.Ctx) error { + mediaType, err := pg.ParseMediaType(ctx.Params("type")) + if err != nil { + return ctx.Status(fiber.StatusInternalServerError).JSON(errorx.RequestParseError) + } + + segment := ctx.Params("segment") + segments, err := c.hashIds.DecodeInt64WithError(segment) + if err != nil { + log.WithField("action", "medias.MediaSegment").WithError(err).Error("DecodeInt64WithError") + return ctx.Status(fiber.StatusInternalServerError).JSON(errorx.RequestParseError) + } + + hash := ctx.Params("hash") + claim := fiber.Locals[*jwt.Claims](ctx, consts.CtxKeyClaim) + log.Debug(claim) + + model, err := c.svc.GetMediaByHash(ctx.Context(), claim.TenantID, hash) + if err != nil { + log.WithField("action", "medias.MediaSegment").WithError(err).Error("GetMediaByHash") + return ctx.Status(fiber.StatusInternalServerError).JSON(errorx.InternalError) + } + + filepath := c.svc.GetSegmentPath(ctx.Context(), mediaType, model.Hash, segments[0]) + return ctx.SendFile(filepath) } diff --git a/backend/modules/medias/provider.gen.go b/backend/modules/medias/provider.gen.go index ad6841b..2030e74 100755 --- a/backend/modules/medias/provider.gen.go +++ b/backend/modules/medias/provider.gen.go @@ -8,14 +8,17 @@ import ( "git.ipao.vip/rogeecn/atom/container" "git.ipao.vip/rogeecn/atom/contracts" "git.ipao.vip/rogeecn/atom/utils/opt" + hashids "github.com/speps/go-hashids/v2" ) func Provide(opts ...opt.Option) error { if err := container.Container.Provide(func( + hashIds *hashids.HashID, svc *Service, ) (*Controller, error) { obj := &Controller{ - svc: svc, + hashIds: hashIds, + svc: svc, } return obj, nil }); err != nil { @@ -38,10 +41,12 @@ func Provide(opts ...opt.Option) error { if err := container.Container.Provide(func( db *sql.DB, + hashIds *hashids.HashID, storageConfig *storage.Config, ) (*Service, error) { obj := &Service{ db: db, + hashIds: hashIds, storageConfig: storageConfig, } if err := obj.Prepare(); err != nil { diff --git a/backend/modules/medias/router.go b/backend/modules/medias/router.go index 5537f75..d8f1ad4 100755 --- a/backend/modules/medias/router.go +++ b/backend/modules/medias/router.go @@ -27,4 +27,6 @@ func (r *Router) Register(router fiber.Router) { group := router.Group(r.Name()) group.Post("", r.controller.List) group.Get(":hash", r.controller.Show) + group.Get(":hash/:type", r.controller.MediaIndex) + group.Get(":hash/:type/:segment.ts", r.controller.MediaSegment) } diff --git a/backend/modules/medias/service.go b/backend/modules/medias/service.go index a591f0f..1c3ae4e 100644 --- a/backend/modules/medias/service.go +++ b/backend/modules/medias/service.go @@ -1,27 +1,34 @@ package medias import ( + "bufio" "context" "database/sql" + "fmt" + "os" "path/filepath" "time" - "backend/common/media_store" "backend/database/models/qvyun/public/model" "backend/database/models/qvyun/public/table" + "backend/pkg/media_store" "backend/pkg/path" "backend/pkg/pg" "backend/providers/storage" . "github.com/go-jet/jet/v2/postgres" + "github.com/grafov/m3u8" "github.com/pkg/errors" "github.com/samber/lo" "github.com/sirupsen/logrus" + hashids "github.com/speps/go-hashids/v2" + "github.com/spf13/cast" ) // @provider:except type Service struct { db *sql.DB + hashIds *hashids.HashID storageConfig *storage.Config log *logrus.Entry `inject:"false"` } @@ -244,3 +251,66 @@ func (svc *Service) Upsert(ctx context.Context, tenantId int64, item media_store return nil } + +// get video m3u8 +func (svc *Service) GetM3U8(ctx context.Context, tenantId int64, types pg.MediaType, hash string, bought bool) (m3u8.Playlist, error) { + log := svc.log.WithField("method", "GetM3U8") + indexPath := filepath.Join(svc.storageConfig.Path, hash, types.String(), "index.m3u8") + log.Infof("m3u8 path: %s", indexPath) + + f, err := os.Open(indexPath) + if err != nil { + return nil, errors.Wrap(err, "open index file") + } + + p, listType, err := m3u8.DecodeFrom(bufio.NewReader(f), true) + if err != nil { + return nil, errors.Wrap(err, "decode index file") + } + + if listType != m3u8.MEDIA { + return nil, errors.New("Invalid media file") + } + + media, ok := p.(*m3u8.MediaPlaylist) + if !ok { + return nil, errors.New("Invalid media playlist") + } + media.Segments = lo.Filter(media.Segments, func(seg *m3u8.MediaSegment, _ int) bool { + return seg != nil + }) + + if !bought { + duration := 0 + for i, seg := range media.Segments { + duration += int(seg.Duration) + if duration >= 55 { + media.Segments = media.Segments[:i] + break + } + } + } + + for _, seg := range media.Segments { + // remove seg.URI ext, only keep the name + name, ext := path.SplitNameExt(seg.URI) + nameId, err := cast.ToInt64E(name) + if err != nil { + return nil, errors.Wrap(err, "cast index to int64") + } + + // get video info + hashID, err := svc.hashIds.EncodeInt64([]int64{nameId}) + if err != nil { + return nil, errors.Wrap(err, "encode hash id") + } + seg.URI = fmt.Sprintf("%s/%s.%s", types, hashID, ext) + } + + return media, nil +} + +// GetSegmentPath +func (svc *Service) GetSegmentPath(ctx context.Context, t pg.MediaType, hash string, segment int64) string { + return filepath.Join(svc.storageConfig.Path, hash, t.String(), fmt.Sprintf("%d.ts", segment)) +} diff --git a/backend/modules/medias/service_test.go b/backend/modules/medias/service_test.go index e450198..b876317 100644 --- a/backend/modules/medias/service_test.go +++ b/backend/modules/medias/service_test.go @@ -4,48 +4,65 @@ import ( "context" "testing" - "backend/database/models/qvyun/public/model" - "backend/database/models/qvyun/public/table" - "backend/fixtures" - dbUtil "backend/pkg/db" + "backend/pkg/path" + "backend/pkg/pg" + "backend/pkg/service/testx" + "backend/providers/hashids" + "backend/providers/postgres" + "backend/providers/storage" - "github.com/samber/lo" . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/suite" + "go.uber.org/dig" ) -func TestService_GetUserBoughtMedias(t *testing.T) { - Convey("TestService_GetUserBoughtMedias", t, func() { - db, err := fixtures.GetDB() - So(err, ShouldBeNil) - defer db.Close() +type ServiceInjectParams struct { + dig.In + Svc *Service +} - So(dbUtil.TruncateAllTables(context.TODO(), db, "user_medias"), ShouldBeNil) +type ServiceTestSuite struct { + suite.Suite + ServiceInjectParams +} - Convey("insert some data", func() { - items := []model.UserMedias{ - {UserID: 1, TenantID: 1, MediaID: 1, Price: 10}, - {UserID: 1, TenantID: 1, MediaID: 2, Price: 10}, - {UserID: 1, TenantID: 1, MediaID: 3, Price: 10}, - } +func Test_DiscoverMedias(t *testing.T) { + providers := testx.Default( + postgres.DefaultProvider(), + storage.DefaultProvider(), + hashids.DefaultProvider(), + ).With( + Provide, + ) - tbl := table.UserMedias - stmt := tbl.INSERT(tbl.UserID, tbl.TenantID, tbl.MediaID, tbl.Price).MODELS(items) - t.Log(stmt.DebugSql()) + testx.Serve(providers, t, func(params ServiceInjectParams) { + suite.Run(t, &ServiceTestSuite{ServiceInjectParams: params}) + }) +} - _, err := stmt.Exec(db) +func (t *ServiceTestSuite) Test_getM3U8() { + FocusConvey("Test_ffmpegVideoToM3U8", t.T(), func() { + Convey("Bought", func() { + hash := "f464a6641a60e2722e4042db8fad2813" + media, err := t.Svc.GetM3U8(context.Background(), 1, pg.MediaTypeVideo, hash, true) So(err, ShouldBeNil) + t.T().Logf("%+v", media) + }) - Convey("get user bought medias", func() { - svc := &Service{db: db} - So(svc.Prepare(), ShouldBeNil) - - ids, err := svc.GetUserBoughtMedias(context.TODO(), 1, 1) - So(err, ShouldBeNil) - - for _, id := range ids { - So(lo.Contains([]int64{1, 2, 3}, id), ShouldBeTrue) - } - }) + FocusConvey("Not Bought", func() { + hash := "f464a6641a60e2722e4042db8fad2813" + media, err := t.Svc.GetM3U8(context.Background(), 1, pg.MediaTypeVideo, hash, false) + So(err, ShouldBeNil) + t.T().Logf("%+v", media) }) }) } + +func (t *ServiceTestSuite) Test_getMediaByHash() { + Convey("Test_getMediaByHash", t.T(), func() { + name, ext := path.SplitNameExt("0.ts") + + So(name, ShouldEqual, "0") + So(ext, ShouldEqual, "ts") + }) +} diff --git a/backend/modules/middlewares/m_jwt_parse.go b/backend/modules/middlewares/m_jwt_parse.go index db86929..e889e60 100644 --- a/backend/modules/middlewares/m_jwt_parse.go +++ b/backend/modules/middlewares/m_jwt_parse.go @@ -1,7 +1,7 @@ package middlewares import ( - "backend/common/consts" + "backend/pkg/consts" "github.com/gofiber/fiber/v3" "github.com/pkg/errors" diff --git a/backend/modules/users/service.go b/backend/modules/users/service.go index 4aa8182..cf07f65 100644 --- a/backend/modules/users/service.go +++ b/backend/modules/users/service.go @@ -5,9 +5,9 @@ import ( "database/sql" "time" - "backend/common/consts" "backend/database/models/qvyun/public/model" "backend/database/models/qvyun/public/table" + "backend/pkg/consts" "backend/pkg/db" "backend/pkg/pg" diff --git a/backend/common/consts/consts.go b/backend/pkg/consts/consts.go similarity index 100% rename from backend/common/consts/consts.go rename to backend/pkg/consts/consts.go diff --git a/backend/common/consts/ctx.gen.go b/backend/pkg/consts/ctx.gen.go similarity index 100% rename from backend/common/consts/ctx.gen.go rename to backend/pkg/consts/ctx.gen.go diff --git a/backend/common/consts/ctx.go b/backend/pkg/consts/ctx.go similarity index 100% rename from backend/common/consts/ctx.go rename to backend/pkg/consts/ctx.go diff --git a/backend/common/dao.go b/backend/pkg/dao.go similarity index 100% rename from backend/common/dao.go rename to backend/pkg/dao.go diff --git a/backend/common/data_structures.go b/backend/pkg/data_structures.go similarity index 100% rename from backend/common/data_structures.go rename to backend/pkg/data_structures.go diff --git a/backend/pkg/db/db.go b/backend/pkg/db/db.go index 106aa2f..1894e68 100644 --- a/backend/pkg/db/db.go +++ b/backend/pkg/db/db.go @@ -5,7 +5,7 @@ import ( "database/sql" "fmt" - "backend/common/consts" + "backend/pkg/consts" "github.com/go-jet/jet/v2/qrm" ) diff --git a/backend/common/errorx/error.go b/backend/pkg/errorx/error.go similarity index 100% rename from backend/common/errorx/error.go rename to backend/pkg/errorx/error.go diff --git a/backend/common/media_store/store.go b/backend/pkg/media_store/store.go similarity index 100% rename from backend/common/media_store/store.go rename to backend/pkg/media_store/store.go diff --git a/backend/pkg/path/fs.go b/backend/pkg/path/fs.go index 9c9db57..8e1b8fb 100644 --- a/backend/pkg/path/fs.go +++ b/backend/pkg/path/fs.go @@ -2,6 +2,8 @@ package path import ( "os" + "path/filepath" + "strings" "github.com/pkg/errors" ) @@ -35,3 +37,10 @@ func DirExists(path string) bool { } return st.IsDir() } + +func SplitNameExt(name string) (string, string) { + ext := filepath.Ext(name) + name = name[:len(name)-len(ext)] + + return name, strings.TrimLeft(ext, ".") +} diff --git a/backend/common/service/http/http.go b/backend/pkg/service/http/http.go similarity index 96% rename from backend/common/service/http/http.go rename to backend/pkg/service/http/http.go index 433b22f..a9dca25 100644 --- a/backend/common/service/http/http.go +++ b/backend/pkg/service/http/http.go @@ -5,6 +5,7 @@ import ( "backend/modules/middlewares" "backend/modules/users" "backend/providers/app" + "backend/providers/hashids" "backend/providers/http" "backend/providers/jwt" "backend/providers/postgres" @@ -25,6 +26,7 @@ func defaultProviders(providers ...container.ProviderContainer) container.Provid http.DefaultProvider(), postgres.DefaultProvider(), jwt.DefaultProvider(), + hashids.DefaultProvider(), }, providers...) } diff --git a/backend/common/service/migrate/migrate.go b/backend/pkg/service/migrate/migrate.go similarity index 100% rename from backend/common/service/migrate/migrate.go rename to backend/pkg/service/migrate/migrate.go diff --git a/backend/common/service/model/gen.go b/backend/pkg/service/model/gen.go similarity index 100% rename from backend/common/service/model/gen.go rename to backend/pkg/service/model/gen.go diff --git a/backend/common/service/tasks/tasks.go b/backend/pkg/service/tasks/tasks.go similarity index 100% rename from backend/common/service/tasks/tasks.go rename to backend/pkg/service/tasks/tasks.go diff --git a/backend/common/service/tenants/tenants.go b/backend/pkg/service/tenants/tenants.go similarity index 100% rename from backend/common/service/tenants/tenants.go rename to backend/pkg/service/tenants/tenants.go diff --git a/backend/common/service/testx/testing.go b/backend/pkg/service/testx/testing.go similarity index 85% rename from backend/common/service/testx/testing.go rename to backend/pkg/service/testx/testing.go index 3a94b42..8d6b50e 100644 --- a/backend/common/service/testx/testing.go +++ b/backend/pkg/service/testx/testing.go @@ -4,8 +4,6 @@ import ( "os" "testing" - "backend/providers/hashids" - "git.ipao.vip/rogeecn/atom" "git.ipao.vip/rogeecn/atom/container" "github.com/rogeecn/fabfile" @@ -13,9 +11,7 @@ import ( ) func Default(providers ...container.ProviderContainer) container.Providers { - return append(container.Providers{ - hashids.DefaultProvider(), - }, providers...) + return append(container.Providers{}, providers...) } func Serve(providers container.Providers, t *testing.T, invoke any) { diff --git a/backend/common/session.go b/backend/pkg/session.go similarity index 100% rename from backend/common/session.go rename to backend/pkg/session.go diff --git a/backend/request.http b/backend/request.http index 3af00cf..88a2ba2 100644 --- a/backend/request.http +++ b/backend/request.http @@ -12,4 +12,12 @@ Authorization: {{ Token }} ### Get Media GET {{host}}/v1/medias/f464a6641a60e2722e4042db8fad2813 Content-Type: application/json +Authorization: {{ Token }} + +### Get media index +GET {{host}}/v1/medias/f464a6641a60e2722e4042db8fad2813/video +Authorization: {{ Token }} + +### Get media segment +GET {{host}}/v1/medias/f464a6641a60e2722e4042db8fad2813/video/Jk0eyPxqlX.ts Authorization: {{ Token }} \ No newline at end of file