mirror of
https://github.com/simon-ding/polaris.git
synced 2026-02-23 12:10:48 +08:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e169172c68 | ||
|
|
937b035634 | ||
|
|
c639e11b90 | ||
|
|
f2ac688ed8 | ||
|
|
369263a55c | ||
|
|
9d4848129f | ||
|
|
f7e82fa464 | ||
|
|
d2354ab33c | ||
|
|
67014cfb16 | ||
|
|
60edeacd0d | ||
|
|
4c77cf5798 | ||
|
|
3cf48d1f8e | ||
|
|
6d127c6d00 | ||
|
|
22f76e3f57 | ||
|
|
e947396c04 | ||
|
|
1020190c01 | ||
|
|
7c05acd1cf | ||
|
|
76a9183b52 | ||
|
|
6698d368c3 |
13
.github/workflows/go.yml
vendored
13
.github/workflows/go.yml
vendored
@@ -29,6 +29,19 @@ jobs:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GHCR_TOKEN }}
|
||||
|
||||
- name: Set up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
flutter-version: 3
|
||||
|
||||
- name: Build Web
|
||||
run: |
|
||||
cd ui
|
||||
flutter pub get
|
||||
flutter build web --no-web-resources-cdn
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
|
||||
18
.github/workflows/release.yml
vendored
18
.github/workflows/release.yml
vendored
@@ -37,12 +37,25 @@ jobs:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GHCR_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
- name: Set up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
flutter-version: 3
|
||||
|
||||
- name: Build Web
|
||||
run: |
|
||||
cd ui
|
||||
flutter pub get
|
||||
flutter build web --no-web-resources-cdn
|
||||
|
||||
- name: Build and push
|
||||
id: push
|
||||
uses: docker/build-push-action@v6
|
||||
@@ -50,10 +63,7 @@ jobs:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
push: true
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/arm64
|
||||
linux/arm/v7
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/s390x,linux/ppc64le
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
FROM instrumentisto/flutter:3 AS flutter
|
||||
WORKDIR /app
|
||||
COPY ./ui/pubspec.yaml ./ui/pubspec.lock ./
|
||||
RUN flutter pub get
|
||||
COPY ./ui/ ./
|
||||
RUN flutter build web --no-web-resources-cdn --web-renderer html
|
||||
|
||||
# 打包依赖阶段使用golang作为基础镜像
|
||||
FROM golang:1.23 as builder
|
||||
|
||||
# 启用go module
|
||||
@@ -20,7 +12,6 @@ RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
COPY --from=flutter /app/build/web ./ui/build/web/
|
||||
# 指定OS等,并go build
|
||||
RUN CGO_ENABLED=0 go build -o polaris -ldflags="-X polaris/db.Version=$(git describe --tags --long)" ./cmd/
|
||||
|
||||
|
||||
@@ -38,6 +38,15 @@
|
||||
- [x] 支持导入plex watchlist,plex里标记,自动导入polaris
|
||||
- [x] and more...
|
||||
|
||||
## 支持的平台
|
||||
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
- linux/arm/v7
|
||||
- linux/386
|
||||
- linux/s390x
|
||||
- linux/ppc64le
|
||||
|
||||
## Todos
|
||||
|
||||
|
||||
|
||||
14
cmd/main.go
14
cmd/main.go
@@ -3,9 +3,7 @@ package main
|
||||
import (
|
||||
"polaris/db"
|
||||
"polaris/log"
|
||||
"polaris/pkg/utils"
|
||||
"polaris/server"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -18,13 +16,13 @@ func main() {
|
||||
log.Panicf("init db error: %v", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
time.Sleep(2 * time.Second)
|
||||
if err := utils.OpenURL("http://127.0.0.1:8080"); err != nil {
|
||||
log.Errorf("open url error: %v", err)
|
||||
}
|
||||
// go func() {
|
||||
// time.Sleep(2 * time.Second)
|
||||
// if err := utils.OpenURL("http://127.0.0.1:8080"); err != nil {
|
||||
// log.Errorf("open url error: %v", err)
|
||||
// }
|
||||
|
||||
}()
|
||||
// }()
|
||||
s := server.NewServer(dbClient)
|
||||
if err := s.Serve(); err != nil {
|
||||
log.Errorf("server start error: %v", err)
|
||||
|
||||
2
db/db.go
2
db/db.go
@@ -157,6 +157,7 @@ func (c *Client) AddMediaWatchlist(m *ent.Media, episodes []int) (*ent.Media, er
|
||||
SetDownloadHistoryEpisodes(m.DownloadHistoryEpisodes).
|
||||
SetLimiter(m.Limiter).
|
||||
SetExtras(m.Extras).
|
||||
SetAlternativeTitles(m.AlternativeTitles).
|
||||
AddEpisodeIDs(episodes...).
|
||||
Save(context.TODO())
|
||||
return r, err
|
||||
@@ -251,6 +252,7 @@ func (c *Client) SaveEposideDetail2(d *ent.Episode) (int, error) {
|
||||
SetMediaID(d.MediaID).
|
||||
SetStatus(d.Status).
|
||||
SetOverview(d.Overview).
|
||||
SetMonitored(d.Monitored).
|
||||
SetTitle(d.Title).Save(context.TODO())
|
||||
|
||||
return ep.ID, err
|
||||
|
||||
15
ent/media.go
15
ent/media.go
@@ -49,6 +49,8 @@ type Media struct {
|
||||
Limiter schema.MediaLimiter `json:"limiter,omitempty"`
|
||||
// Extras holds the value of the "extras" field.
|
||||
Extras schema.MediaExtras `json:"extras,omitempty"`
|
||||
// AlternativeTitles holds the value of the "alternative_titles" field.
|
||||
AlternativeTitles []schema.AlternativeTilte `json:"alternative_titles,omitempty"`
|
||||
// Edges holds the relations/edges for other nodes in the graph.
|
||||
// The values are being populated by the MediaQuery when eager-loading is set.
|
||||
Edges MediaEdges `json:"edges"`
|
||||
@@ -78,7 +80,7 @@ func (*Media) scanValues(columns []string) ([]any, error) {
|
||||
values := make([]any, len(columns))
|
||||
for i := range columns {
|
||||
switch columns[i] {
|
||||
case media.FieldLimiter, media.FieldExtras:
|
||||
case media.FieldLimiter, media.FieldExtras, media.FieldAlternativeTitles:
|
||||
values[i] = new([]byte)
|
||||
case media.FieldDownloadHistoryEpisodes:
|
||||
values[i] = new(sql.NullBool)
|
||||
@@ -203,6 +205,14 @@ func (m *Media) assignValues(columns []string, values []any) error {
|
||||
return fmt.Errorf("unmarshal field extras: %w", err)
|
||||
}
|
||||
}
|
||||
case media.FieldAlternativeTitles:
|
||||
if value, ok := values[i].(*[]byte); !ok {
|
||||
return fmt.Errorf("unexpected type %T for field alternative_titles", values[i])
|
||||
} else if value != nil && len(*value) > 0 {
|
||||
if err := json.Unmarshal(*value, &m.AlternativeTitles); err != nil {
|
||||
return fmt.Errorf("unmarshal field alternative_titles: %w", err)
|
||||
}
|
||||
}
|
||||
default:
|
||||
m.selectValues.Set(columns[i], values[i])
|
||||
}
|
||||
@@ -288,6 +298,9 @@ func (m *Media) String() string {
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("extras=")
|
||||
builder.WriteString(fmt.Sprintf("%v", m.Extras))
|
||||
builder.WriteString(", ")
|
||||
builder.WriteString("alternative_titles=")
|
||||
builder.WriteString(fmt.Sprintf("%v", m.AlternativeTitles))
|
||||
builder.WriteByte(')')
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
@@ -45,6 +45,8 @@ const (
|
||||
FieldLimiter = "limiter"
|
||||
// FieldExtras holds the string denoting the extras field in the database.
|
||||
FieldExtras = "extras"
|
||||
// FieldAlternativeTitles holds the string denoting the alternative_titles field in the database.
|
||||
FieldAlternativeTitles = "alternative_titles"
|
||||
// EdgeEpisodes holds the string denoting the episodes edge name in mutations.
|
||||
EdgeEpisodes = "episodes"
|
||||
// Table holds the table name of the media in the database.
|
||||
@@ -76,6 +78,7 @@ var Columns = []string{
|
||||
FieldDownloadHistoryEpisodes,
|
||||
FieldLimiter,
|
||||
FieldExtras,
|
||||
FieldAlternativeTitles,
|
||||
}
|
||||
|
||||
// ValidColumn reports if the column name is valid (part of the table columns).
|
||||
|
||||
@@ -795,6 +795,16 @@ func ExtrasNotNil() predicate.Media {
|
||||
return predicate.Media(sql.FieldNotNull(FieldExtras))
|
||||
}
|
||||
|
||||
// AlternativeTitlesIsNil applies the IsNil predicate on the "alternative_titles" field.
|
||||
func AlternativeTitlesIsNil() predicate.Media {
|
||||
return predicate.Media(sql.FieldIsNull(FieldAlternativeTitles))
|
||||
}
|
||||
|
||||
// AlternativeTitlesNotNil applies the NotNil predicate on the "alternative_titles" field.
|
||||
func AlternativeTitlesNotNil() predicate.Media {
|
||||
return predicate.Media(sql.FieldNotNull(FieldAlternativeTitles))
|
||||
}
|
||||
|
||||
// HasEpisodes applies the HasEdge predicate on the "episodes" edge.
|
||||
func HasEpisodes() predicate.Media {
|
||||
return predicate.Media(func(s *sql.Selector) {
|
||||
|
||||
@@ -184,6 +184,12 @@ func (mc *MediaCreate) SetNillableExtras(se *schema.MediaExtras) *MediaCreate {
|
||||
return mc
|
||||
}
|
||||
|
||||
// SetAlternativeTitles sets the "alternative_titles" field.
|
||||
func (mc *MediaCreate) SetAlternativeTitles(st []schema.AlternativeTilte) *MediaCreate {
|
||||
mc.mutation.SetAlternativeTitles(st)
|
||||
return mc
|
||||
}
|
||||
|
||||
// AddEpisodeIDs adds the "episodes" edge to the Episode entity by IDs.
|
||||
func (mc *MediaCreate) AddEpisodeIDs(ids ...int) *MediaCreate {
|
||||
mc.mutation.AddEpisodeIDs(ids...)
|
||||
@@ -377,6 +383,10 @@ func (mc *MediaCreate) createSpec() (*Media, *sqlgraph.CreateSpec) {
|
||||
_spec.SetField(media.FieldExtras, field.TypeJSON, value)
|
||||
_node.Extras = value
|
||||
}
|
||||
if value, ok := mc.mutation.AlternativeTitles(); ok {
|
||||
_spec.SetField(media.FieldAlternativeTitles, field.TypeJSON, value)
|
||||
_node.AlternativeTitles = value
|
||||
}
|
||||
if nodes := mc.mutation.EpisodesIDs(); len(nodes) > 0 {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.O2M,
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"entgo.io/ent/dialect/sql/sqlgraph"
|
||||
"entgo.io/ent/dialect/sql/sqljson"
|
||||
"entgo.io/ent/schema/field"
|
||||
)
|
||||
|
||||
@@ -290,6 +291,24 @@ func (mu *MediaUpdate) ClearExtras() *MediaUpdate {
|
||||
return mu
|
||||
}
|
||||
|
||||
// SetAlternativeTitles sets the "alternative_titles" field.
|
||||
func (mu *MediaUpdate) SetAlternativeTitles(st []schema.AlternativeTilte) *MediaUpdate {
|
||||
mu.mutation.SetAlternativeTitles(st)
|
||||
return mu
|
||||
}
|
||||
|
||||
// AppendAlternativeTitles appends st to the "alternative_titles" field.
|
||||
func (mu *MediaUpdate) AppendAlternativeTitles(st []schema.AlternativeTilte) *MediaUpdate {
|
||||
mu.mutation.AppendAlternativeTitles(st)
|
||||
return mu
|
||||
}
|
||||
|
||||
// ClearAlternativeTitles clears the value of the "alternative_titles" field.
|
||||
func (mu *MediaUpdate) ClearAlternativeTitles() *MediaUpdate {
|
||||
mu.mutation.ClearAlternativeTitles()
|
||||
return mu
|
||||
}
|
||||
|
||||
// AddEpisodeIDs adds the "episodes" edge to the Episode entity by IDs.
|
||||
func (mu *MediaUpdate) AddEpisodeIDs(ids ...int) *MediaUpdate {
|
||||
mu.mutation.AddEpisodeIDs(ids...)
|
||||
@@ -454,6 +473,17 @@ func (mu *MediaUpdate) sqlSave(ctx context.Context) (n int, err error) {
|
||||
if mu.mutation.ExtrasCleared() {
|
||||
_spec.ClearField(media.FieldExtras, field.TypeJSON)
|
||||
}
|
||||
if value, ok := mu.mutation.AlternativeTitles(); ok {
|
||||
_spec.SetField(media.FieldAlternativeTitles, field.TypeJSON, value)
|
||||
}
|
||||
if value, ok := mu.mutation.AppendedAlternativeTitles(); ok {
|
||||
_spec.AddModifier(func(u *sql.UpdateBuilder) {
|
||||
sqljson.Append(u, media.FieldAlternativeTitles, value)
|
||||
})
|
||||
}
|
||||
if mu.mutation.AlternativeTitlesCleared() {
|
||||
_spec.ClearField(media.FieldAlternativeTitles, field.TypeJSON)
|
||||
}
|
||||
if mu.mutation.EpisodesCleared() {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.O2M,
|
||||
@@ -779,6 +809,24 @@ func (muo *MediaUpdateOne) ClearExtras() *MediaUpdateOne {
|
||||
return muo
|
||||
}
|
||||
|
||||
// SetAlternativeTitles sets the "alternative_titles" field.
|
||||
func (muo *MediaUpdateOne) SetAlternativeTitles(st []schema.AlternativeTilte) *MediaUpdateOne {
|
||||
muo.mutation.SetAlternativeTitles(st)
|
||||
return muo
|
||||
}
|
||||
|
||||
// AppendAlternativeTitles appends st to the "alternative_titles" field.
|
||||
func (muo *MediaUpdateOne) AppendAlternativeTitles(st []schema.AlternativeTilte) *MediaUpdateOne {
|
||||
muo.mutation.AppendAlternativeTitles(st)
|
||||
return muo
|
||||
}
|
||||
|
||||
// ClearAlternativeTitles clears the value of the "alternative_titles" field.
|
||||
func (muo *MediaUpdateOne) ClearAlternativeTitles() *MediaUpdateOne {
|
||||
muo.mutation.ClearAlternativeTitles()
|
||||
return muo
|
||||
}
|
||||
|
||||
// AddEpisodeIDs adds the "episodes" edge to the Episode entity by IDs.
|
||||
func (muo *MediaUpdateOne) AddEpisodeIDs(ids ...int) *MediaUpdateOne {
|
||||
muo.mutation.AddEpisodeIDs(ids...)
|
||||
@@ -973,6 +1021,17 @@ func (muo *MediaUpdateOne) sqlSave(ctx context.Context) (_node *Media, err error
|
||||
if muo.mutation.ExtrasCleared() {
|
||||
_spec.ClearField(media.FieldExtras, field.TypeJSON)
|
||||
}
|
||||
if value, ok := muo.mutation.AlternativeTitles(); ok {
|
||||
_spec.SetField(media.FieldAlternativeTitles, field.TypeJSON, value)
|
||||
}
|
||||
if value, ok := muo.mutation.AppendedAlternativeTitles(); ok {
|
||||
_spec.AddModifier(func(u *sql.UpdateBuilder) {
|
||||
sqljson.Append(u, media.FieldAlternativeTitles, value)
|
||||
})
|
||||
}
|
||||
if muo.mutation.AlternativeTitlesCleared() {
|
||||
_spec.ClearField(media.FieldAlternativeTitles, field.TypeJSON)
|
||||
}
|
||||
if muo.mutation.EpisodesCleared() {
|
||||
edge := &sqlgraph.EdgeSpec{
|
||||
Rel: sqlgraph.O2M,
|
||||
|
||||
@@ -143,6 +143,7 @@ var (
|
||||
{Name: "download_history_episodes", Type: field.TypeBool, Nullable: true, Default: false},
|
||||
{Name: "limiter", Type: field.TypeJSON, Nullable: true},
|
||||
{Name: "extras", Type: field.TypeJSON, Nullable: true},
|
||||
{Name: "alternative_titles", Type: field.TypeJSON, Nullable: true},
|
||||
}
|
||||
// MediaTable holds the schema information for the "media" table.
|
||||
MediaTable = &schema.Table{
|
||||
|
||||
@@ -5115,6 +5115,8 @@ type MediaMutation struct {
|
||||
download_history_episodes *bool
|
||||
limiter *schema.MediaLimiter
|
||||
extras *schema.MediaExtras
|
||||
alternative_titles *[]schema.AlternativeTilte
|
||||
appendalternative_titles []schema.AlternativeTilte
|
||||
clearedFields map[string]struct{}
|
||||
episodes map[int]struct{}
|
||||
removedepisodes map[int]struct{}
|
||||
@@ -5881,6 +5883,71 @@ func (m *MediaMutation) ResetExtras() {
|
||||
delete(m.clearedFields, media.FieldExtras)
|
||||
}
|
||||
|
||||
// SetAlternativeTitles sets the "alternative_titles" field.
|
||||
func (m *MediaMutation) SetAlternativeTitles(st []schema.AlternativeTilte) {
|
||||
m.alternative_titles = &st
|
||||
m.appendalternative_titles = nil
|
||||
}
|
||||
|
||||
// AlternativeTitles returns the value of the "alternative_titles" field in the mutation.
|
||||
func (m *MediaMutation) AlternativeTitles() (r []schema.AlternativeTilte, exists bool) {
|
||||
v := m.alternative_titles
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldAlternativeTitles returns the old "alternative_titles" field's value of the Media entity.
|
||||
// If the Media object wasn't provided to the builder, the object is fetched from the database.
|
||||
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||
func (m *MediaMutation) OldAlternativeTitles(ctx context.Context) (v []schema.AlternativeTilte, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldAlternativeTitles is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldAlternativeTitles requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldAlternativeTitles: %w", err)
|
||||
}
|
||||
return oldValue.AlternativeTitles, nil
|
||||
}
|
||||
|
||||
// AppendAlternativeTitles adds st to the "alternative_titles" field.
|
||||
func (m *MediaMutation) AppendAlternativeTitles(st []schema.AlternativeTilte) {
|
||||
m.appendalternative_titles = append(m.appendalternative_titles, st...)
|
||||
}
|
||||
|
||||
// AppendedAlternativeTitles returns the list of values that were appended to the "alternative_titles" field in this mutation.
|
||||
func (m *MediaMutation) AppendedAlternativeTitles() ([]schema.AlternativeTilte, bool) {
|
||||
if len(m.appendalternative_titles) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
return m.appendalternative_titles, true
|
||||
}
|
||||
|
||||
// ClearAlternativeTitles clears the value of the "alternative_titles" field.
|
||||
func (m *MediaMutation) ClearAlternativeTitles() {
|
||||
m.alternative_titles = nil
|
||||
m.appendalternative_titles = nil
|
||||
m.clearedFields[media.FieldAlternativeTitles] = struct{}{}
|
||||
}
|
||||
|
||||
// AlternativeTitlesCleared returns if the "alternative_titles" field was cleared in this mutation.
|
||||
func (m *MediaMutation) AlternativeTitlesCleared() bool {
|
||||
_, ok := m.clearedFields[media.FieldAlternativeTitles]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ResetAlternativeTitles resets all changes to the "alternative_titles" field.
|
||||
func (m *MediaMutation) ResetAlternativeTitles() {
|
||||
m.alternative_titles = nil
|
||||
m.appendalternative_titles = nil
|
||||
delete(m.clearedFields, media.FieldAlternativeTitles)
|
||||
}
|
||||
|
||||
// AddEpisodeIDs adds the "episodes" edge to the Episode entity by ids.
|
||||
func (m *MediaMutation) AddEpisodeIDs(ids ...int) {
|
||||
if m.episodes == nil {
|
||||
@@ -5969,7 +6036,7 @@ func (m *MediaMutation) Type() string {
|
||||
// order to get all numeric fields that were incremented/decremented, call
|
||||
// AddedFields().
|
||||
func (m *MediaMutation) Fields() []string {
|
||||
fields := make([]string, 0, 15)
|
||||
fields := make([]string, 0, 16)
|
||||
if m.tmdb_id != nil {
|
||||
fields = append(fields, media.FieldTmdbID)
|
||||
}
|
||||
@@ -6015,6 +6082,9 @@ func (m *MediaMutation) Fields() []string {
|
||||
if m.extras != nil {
|
||||
fields = append(fields, media.FieldExtras)
|
||||
}
|
||||
if m.alternative_titles != nil {
|
||||
fields = append(fields, media.FieldAlternativeTitles)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
@@ -6053,6 +6123,8 @@ func (m *MediaMutation) Field(name string) (ent.Value, bool) {
|
||||
return m.Limiter()
|
||||
case media.FieldExtras:
|
||||
return m.Extras()
|
||||
case media.FieldAlternativeTitles:
|
||||
return m.AlternativeTitles()
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
@@ -6092,6 +6164,8 @@ func (m *MediaMutation) OldField(ctx context.Context, name string) (ent.Value, e
|
||||
return m.OldLimiter(ctx)
|
||||
case media.FieldExtras:
|
||||
return m.OldExtras(ctx)
|
||||
case media.FieldAlternativeTitles:
|
||||
return m.OldAlternativeTitles(ctx)
|
||||
}
|
||||
return nil, fmt.Errorf("unknown Media field %s", name)
|
||||
}
|
||||
@@ -6206,6 +6280,13 @@ func (m *MediaMutation) SetField(name string, value ent.Value) error {
|
||||
}
|
||||
m.SetExtras(v)
|
||||
return nil
|
||||
case media.FieldAlternativeTitles:
|
||||
v, ok := value.([]schema.AlternativeTilte)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetAlternativeTitles(v)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unknown Media field %s", name)
|
||||
}
|
||||
@@ -6281,6 +6362,9 @@ func (m *MediaMutation) ClearedFields() []string {
|
||||
if m.FieldCleared(media.FieldExtras) {
|
||||
fields = append(fields, media.FieldExtras)
|
||||
}
|
||||
if m.FieldCleared(media.FieldAlternativeTitles) {
|
||||
fields = append(fields, media.FieldAlternativeTitles)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
@@ -6313,6 +6397,9 @@ func (m *MediaMutation) ClearField(name string) error {
|
||||
case media.FieldExtras:
|
||||
m.ClearExtras()
|
||||
return nil
|
||||
case media.FieldAlternativeTitles:
|
||||
m.ClearAlternativeTitles()
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unknown Media nullable field %s", name)
|
||||
}
|
||||
@@ -6366,6 +6453,9 @@ func (m *MediaMutation) ResetField(name string) error {
|
||||
case media.FieldExtras:
|
||||
m.ResetExtras()
|
||||
return nil
|
||||
case media.FieldAlternativeTitles:
|
||||
m.ResetAlternativeTitles()
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unknown Media field %s", name)
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ func (Media) Fields() []ent.Field {
|
||||
field.Bool("download_history_episodes").Optional().Default(false).Comment("tv series only"),
|
||||
field.JSON("limiter", MediaLimiter{}).Optional(),
|
||||
field.JSON("extras", MediaExtras{}).Optional(),
|
||||
field.JSON("alternative_titles", []AlternativeTilte{}).Optional(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +42,12 @@ func (Media) Edges() []ent.Edge {
|
||||
}
|
||||
}
|
||||
|
||||
type AlternativeTilte struct {
|
||||
Iso3166_1 string `json:"iso_3166_1"`
|
||||
Title string `json:"title"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type MediaLimiter struct {
|
||||
SizeMin int64 `json:"size_min"` //in B
|
||||
SizeMax int64 `json:"size_max"` //in B
|
||||
|
||||
8
go.mod
8
go.mod
@@ -47,7 +47,7 @@ require (
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
|
||||
github.com/tetratelabs/wazero v1.8.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
lukechampine.com/blake3 v1.1.6 // indirect
|
||||
@@ -95,11 +95,11 @@ require (
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/zclconf/go-cty v1.8.0 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.27.0
|
||||
golang.org/x/crypto v0.31.0
|
||||
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948
|
||||
golang.org/x/mod v0.20.0 // indirect
|
||||
golang.org/x/sys v0.25.0
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
golang.org/x/sys v0.28.0
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
16
go.sum
16
go.sum
@@ -362,8 +362,8 @@ golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA=
|
||||
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||
@@ -403,8 +403,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -423,8 +423,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
@@ -436,8 +436,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
||||
@@ -72,3 +72,7 @@ func (a *Alist) UploadProgress() float64 {
|
||||
}
|
||||
return a.progresser()
|
||||
}
|
||||
|
||||
func (a *Alist) RemoveAll(path string) error {
|
||||
return nil
|
||||
}
|
||||
@@ -20,6 +20,7 @@ type Storage interface {
|
||||
ReadFile(string) ([]byte, error)
|
||||
WriteFile(string, []byte) error
|
||||
UploadProgress() float64
|
||||
RemoveAll(path string) error
|
||||
}
|
||||
|
||||
type uploadFunc func(destPath string, destInfo fs.FileInfo, srcReader io.Reader, mimeType *mimetype.MIME) error
|
||||
|
||||
@@ -71,3 +71,7 @@ func (l *LocalStorage) WriteFile(name string, data []byte) error {
|
||||
func (l *LocalStorage) UploadProgress() float64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (l *LocalStorage) RemoveAll(path string) error {
|
||||
return os.RemoveAll(filepath.Join(l.dir, path))
|
||||
}
|
||||
@@ -85,3 +85,7 @@ func (w *WebdavStorage) UploadProgress() float64 {
|
||||
}
|
||||
return w.progresser()
|
||||
}
|
||||
|
||||
func (w *WebdavStorage) RemoveAll(path string) error {
|
||||
return w.fs.RemoveAll(filepath.Join(w.dir, path))
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ func (c *Client) registerCronJob(name string, cron string, f func() error) {
|
||||
func (c *Client) Init() {
|
||||
go c.reloadTasks()
|
||||
c.addSysCron()
|
||||
go c.checkW500PosterOnStartup()
|
||||
}
|
||||
|
||||
func (c *Client) reloadTasks() {
|
||||
|
||||
@@ -149,6 +149,11 @@ func (c *Client) AddTv2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
||||
|
||||
log.Debugf("latest season is %v", lastSeason)
|
||||
|
||||
alterTitles, err := c.getAlterTitles(in.TmdbID, media.MediaTypeTv)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get alter titles")
|
||||
}
|
||||
|
||||
var epIds []int
|
||||
for _, season := range detail.Seasons {
|
||||
seasonId := season.SeasonNumber
|
||||
@@ -195,6 +200,7 @@ func (c *Client) AddTv2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
||||
epIds = append(epIds, epid)
|
||||
}
|
||||
}
|
||||
|
||||
m := &ent.Media{
|
||||
TmdbID: int(detail.ID),
|
||||
ImdbID: detail.IMDbID,
|
||||
@@ -213,6 +219,7 @@ func (c *Client) AddTv2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
||||
OriginalLanguage: detail.OriginalLanguage,
|
||||
Genres: detail.Genres,
|
||||
},
|
||||
AlternativeTitles: alterTitles,
|
||||
}
|
||||
|
||||
r, err := c.db.AddMediaWatchlist(m, epIds)
|
||||
@@ -223,6 +230,10 @@ func (c *Client) AddTv2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
||||
if err := c.downloadPoster(detail.PosterPath, r.ID); err != nil {
|
||||
log.Errorf("download poster error: %v", err)
|
||||
}
|
||||
if err := c.downloadW500Poster(detail.PosterPath, r.ID); err != nil {
|
||||
log.Errorf("download w500 poster error: %v", err)
|
||||
}
|
||||
|
||||
if err := c.downloadBackdrop(detail.BackdropPath, r.ID); err != nil {
|
||||
log.Errorf("download poster error: %v", err)
|
||||
}
|
||||
@@ -236,6 +247,42 @@ func (c *Client) AddTv2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *Client) getAlterTitles(tmdbId int, mediaType media.MediaType) ([]schema.AlternativeTilte, error){
|
||||
var titles []schema.AlternativeTilte
|
||||
|
||||
if mediaType == media.MediaTypeTv {
|
||||
alterTitles, err := c.MustTMDB().GetTVAlternativeTitles(tmdbId, c.language)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "tmdb")
|
||||
}
|
||||
|
||||
for _, t := range alterTitles.Results {
|
||||
titles = append(titles, schema.AlternativeTilte{
|
||||
Iso3166_1: t.Iso3166_1,
|
||||
Title: t.Title,
|
||||
Type: t.Type,
|
||||
})
|
||||
}
|
||||
|
||||
} else if mediaType == media.MediaTypeMovie {
|
||||
alterTitles, err := c.MustTMDB().GetMovieAlternativeTitles(tmdbId, c.language)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "tmdb")
|
||||
}
|
||||
|
||||
for _, t := range alterTitles.Titles {
|
||||
titles = append(titles, schema.AlternativeTilte{
|
||||
Iso3166_1: t.Iso3166_1,
|
||||
Title: t.Title,
|
||||
Type: t.Type,
|
||||
})
|
||||
}
|
||||
}
|
||||
log.Debugf("get alternative titles: %+v", titles)
|
||||
|
||||
return titles, nil
|
||||
}
|
||||
|
||||
func (c *Client) AddMovie2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
||||
log.Infof("add movie watchlist input: %+v", in)
|
||||
detailCn, err := c.MustTMDB().GetMovieDetails(in.TmdbID, db.LanguageCN)
|
||||
@@ -254,6 +301,12 @@ func (c *Client) AddMovie2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
||||
}
|
||||
log.Infof("find detail for movie id %d: %v", in.TmdbID, detail)
|
||||
|
||||
alterTitles, err := c.getAlterTitles(in.TmdbID, media.MediaTypeMovie)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "get alter titles")
|
||||
}
|
||||
|
||||
|
||||
epid, err := c.db.SaveEposideDetail(&ent.Episode{
|
||||
SeasonNumber: 1,
|
||||
EpisodeNumber: 1,
|
||||
@@ -280,6 +333,7 @@ func (c *Client) AddMovie2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
||||
StorageID: in.StorageID,
|
||||
TargetDir: in.Folder,
|
||||
Limiter: schema.MediaLimiter{SizeMin: in.SizeMin, SizeMax: in.SizeMax},
|
||||
AlternativeTitles: alterTitles,
|
||||
}
|
||||
|
||||
extras := schema.MediaExtras{
|
||||
@@ -301,6 +355,10 @@ func (c *Client) AddMovie2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
||||
if err := c.downloadPoster(detail.PosterPath, r.ID); err != nil {
|
||||
log.Errorf("download poster error: %v", err)
|
||||
}
|
||||
if err := c.downloadW500Poster(detail.PosterPath, r.ID); err != nil {
|
||||
log.Errorf("download w500 poster error: %v", err)
|
||||
}
|
||||
|
||||
if err := c.downloadBackdrop(detail.BackdropPath, r.ID); err != nil {
|
||||
log.Errorf("download backdrop error: %v", err)
|
||||
}
|
||||
@@ -315,7 +373,7 @@ func (c *Client) AddMovie2Watchlist(in AddWatchlistIn) (interface{}, error) {
|
||||
}
|
||||
|
||||
func (c *Client) checkMovieFolder(m *ent.Media) error {
|
||||
var storageImpl, err = c.getStorage(m.StorageID, media.MediaTypeMovie)
|
||||
var storageImpl, err = c.GetStorage(m.StorageID, media.MediaTypeMovie)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -372,6 +430,11 @@ func (c *Client) downloadPoster(path string, mediaID int) error {
|
||||
return c.downloadImage(url, mediaID, "poster.jpg")
|
||||
}
|
||||
|
||||
func (c *Client) downloadW500Poster(path string, mediaID int) error {
|
||||
url := "https://image.tmdb.org/t/p/w500" + path
|
||||
return c.downloadImage(url, mediaID, "poster_w500.jpg")
|
||||
}
|
||||
|
||||
func (c *Client) downloadImage(url string, mediaID int, name string) error {
|
||||
|
||||
log.Infof("try to download image: %v", url)
|
||||
@@ -397,6 +460,46 @@ func (c *Client) downloadImage(url string, mediaID int, name string) error {
|
||||
|
||||
}
|
||||
|
||||
func (c *Client) checkW500PosterOnStartup() {
|
||||
log.Infof("check all w500 posters")
|
||||
all := c.db.GetMediaWatchlist(media.MediaTypeTv)
|
||||
movies := c.db.GetMediaWatchlist(media.MediaTypeMovie)
|
||||
all = append(all, movies...)
|
||||
for _, e := range all {
|
||||
targetFile := filepath.Join(fmt.Sprintf("%v/%d", db.ImgPath, e.ID), "poster_w500.jpg")
|
||||
if _, err := os.Stat(targetFile); err != nil {
|
||||
log.Infof("poster_w500.jpg not exist for %s, will download it", e.NameEn)
|
||||
|
||||
if e.MediaType ==media.MediaTypeTv {
|
||||
detail, err := c.MustTMDB().GetTvDetails(e.TmdbID, db.LanguageCN)
|
||||
if err != nil {
|
||||
log.Warnf("get tmdb detail for %s error: %v", e.NameEn, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := c.downloadW500Poster(detail.PosterPath, e.ID); err != nil {
|
||||
log.Warnf("download w500 poster error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
} else {
|
||||
detail, err := c.MustTMDB().GetMovieDetails(e.TmdbID, db.LanguageCN)
|
||||
if err != nil {
|
||||
log.Warnf("get tmdb detail for %s error: %v", e.NameEn, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := c.downloadW500Poster(detail.PosterPath, e.ID); err != nil {
|
||||
log.Warnf("download w500 poster error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) SuggestedMovieFolderName(tmdbId int) (string, error) {
|
||||
|
||||
d1, err := c.MustTMDB().GetMovieDetails(tmdbId, c.language)
|
||||
|
||||
@@ -35,7 +35,7 @@ func (c *Client) writeNfoFile(historyId int) error {
|
||||
}
|
||||
|
||||
if md.MediaType == media.MediaTypeTv { //tvshow.nfo
|
||||
st, err := c.getStorage(md.StorageID, media.MediaTypeTv)
|
||||
st, err := c.GetStorage(md.StorageID, media.MediaTypeTv)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get storage")
|
||||
}
|
||||
@@ -70,7 +70,7 @@ func (c *Client) writeNfoFile(historyId int) error {
|
||||
}
|
||||
|
||||
} else if md.MediaType == media.MediaTypeMovie { //movie.nfo
|
||||
st, err := c.getStorage(md.StorageID, media.MediaTypeMovie)
|
||||
st, err := c.GetStorage(md.StorageID, media.MediaTypeMovie)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get storage")
|
||||
}
|
||||
@@ -122,7 +122,7 @@ func (c *Client) writePlexmatch(historyId int) error {
|
||||
if series.MediaType != media.MediaTypeTv { //.plexmatch only support tv series
|
||||
return nil
|
||||
}
|
||||
st, err := c.getStorage(series.StorageID, media.MediaTypeTv)
|
||||
st, err := c.GetStorage(series.StorageID, media.MediaTypeTv)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "get storage")
|
||||
}
|
||||
@@ -197,7 +197,7 @@ func (c *Client) nfoSupportEnabled() bool {
|
||||
return c.db.GetSetting(db.SettingNfoSupportEnabled) == "true"
|
||||
}
|
||||
|
||||
func (c *Client) getStorage(storageId int, mediaType media.MediaType) (storage.Storage, error) {
|
||||
func (c *Client) GetStorage(storageId int, mediaType media.MediaType) (storage.Storage, error) {
|
||||
st := c.db.GetStorage(storageId)
|
||||
targetPath := st.TvPath
|
||||
if mediaType == media.MediaTypeMovie {
|
||||
|
||||
@@ -203,7 +203,7 @@ func (c *Client) moveCompletedTask(id int) (err1 error) {
|
||||
}
|
||||
st := c.db.GetStorage(series.StorageID)
|
||||
log.Infof("move task files to target dir: %v", r.TargetDir)
|
||||
stImpl, err := c.getStorage(st.ID, series.MediaType)
|
||||
stImpl, err := c.GetStorage(st.ID, series.MediaType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -243,7 +243,7 @@ func (c *Client) CheckDownloadedSeriesFiles(m *ent.Media) error {
|
||||
}
|
||||
log.Infof("check files in directory: %s", m.TargetDir)
|
||||
|
||||
var storageImpl, err = c.getStorage(m.StorageID, media.MediaTypeTv)
|
||||
var storageImpl, err = c.GetStorage(m.StorageID, media.MediaTypeTv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package core
|
||||
import (
|
||||
"fmt"
|
||||
"polaris/db"
|
||||
"polaris/ent"
|
||||
"polaris/ent/media"
|
||||
"polaris/log"
|
||||
"polaris/pkg/metadata"
|
||||
@@ -26,6 +27,51 @@ type SearchParam struct {
|
||||
FilterQiangban bool //for movie, 是否过滤枪版电影
|
||||
}
|
||||
|
||||
func names2Query(media *ent.Media) []string {
|
||||
var names = []string{media.NameEn}
|
||||
|
||||
if media.NameCn != "" {
|
||||
hasName := false
|
||||
for _, n := range names {
|
||||
if media.NameCn == n {
|
||||
hasName = true
|
||||
}
|
||||
}
|
||||
if !hasName {
|
||||
names = append(names, media.NameCn)
|
||||
}
|
||||
|
||||
}
|
||||
if media.OriginalName != "" {
|
||||
hasName := false
|
||||
for _, n := range names {
|
||||
if media.OriginalName == n {
|
||||
hasName = true
|
||||
}
|
||||
}
|
||||
if !hasName {
|
||||
names = append(names, media.OriginalName)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for _, t := range media.AlternativeTitles {
|
||||
if (t.Iso3166_1 == "CN" || t.Iso3166_1 == "US") && t.Type == "" {
|
||||
hasName := false
|
||||
for _, n := range names {
|
||||
if t.Title == n {
|
||||
hasName = true
|
||||
}
|
||||
}
|
||||
if !hasName {
|
||||
names = append(names, t.Title)
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Debugf("name to query %+v", names)
|
||||
return names
|
||||
}
|
||||
|
||||
func SearchTvSeries(db1 *db.Client, param *SearchParam) ([]torznab.Result, error) {
|
||||
series := db1.GetMediaDetails(param.MediaId)
|
||||
if series == nil {
|
||||
@@ -38,7 +84,9 @@ func SearchTvSeries(db1 *db.Client, param *SearchParam) ([]torznab.Result, error
|
||||
}
|
||||
log.Debugf("check tv series %s, season %d, episode %v", series.NameEn, param.SeasonNum, param.Episodes)
|
||||
|
||||
res := searchWithTorznab(db1, prowlarr.TV, series.NameEn, series.NameCn, series.OriginalName)
|
||||
names := names2Query(series.Media)
|
||||
|
||||
res := searchWithTorznab(db1, prowlarr.TV, names...)
|
||||
|
||||
var filtered []torznab.Result
|
||||
lo:
|
||||
@@ -119,8 +167,8 @@ func imdbIDMatchExact(id1, id2 string) bool {
|
||||
return id1 == id2
|
||||
}
|
||||
|
||||
func torrentSizeOk(detail *db.MediaDetails, globalLimiter *db.MediaSizeLimiter, torrentSize int64,
|
||||
torrentEpisodeNum int, param *SearchParam) bool {
|
||||
func torrentSizeOk(detail *db.MediaDetails, globalLimiter *db.MediaSizeLimiter, torrentSize int64,
|
||||
torrentEpisodeNum int, param *SearchParam) bool {
|
||||
|
||||
multiplier := 1 //大小倍数,正常为1,如果是季包则为季内集数
|
||||
if detail.MediaType == media.MediaTypeTv {
|
||||
@@ -198,8 +246,9 @@ func SearchMovie(db1 *db.Client, param *SearchParam) ([]torznab.Result, error) {
|
||||
log.Warnf("get tv size limiter: %v", err)
|
||||
limiter = &db.MediaSizeLimiter{}
|
||||
}
|
||||
names := names2Query(movieDetail.Media)
|
||||
|
||||
res := searchWithTorznab(db1, prowlarr.Movie, movieDetail.NameEn, movieDetail.NameCn, movieDetail.OriginalName)
|
||||
res := searchWithTorznab(db1, prowlarr.Movie, names...)
|
||||
if movieDetail.Extras.IsJav() {
|
||||
res1 := searchWithTorznab(db1, prowlarr.Movie, movieDetail.Extras.JavId)
|
||||
res = append(res, res1...)
|
||||
@@ -361,5 +410,7 @@ func torrentNameOk(detail *db.MediaDetails, tester NameTester) bool {
|
||||
if detail.Extras.IsJav() && tester.IsAcceptable(detail.Extras.JavId) {
|
||||
return true
|
||||
}
|
||||
return tester.IsAcceptable(detail.NameCn, detail.NameEn, detail.OriginalName)
|
||||
names := names2Query(detail.Media)
|
||||
|
||||
return tester.IsAcceptable(names...)
|
||||
}
|
||||
|
||||
@@ -7,9 +7,11 @@ import (
|
||||
"net/url"
|
||||
"polaris/db"
|
||||
"polaris/log"
|
||||
"polaris/pkg/cache"
|
||||
"polaris/pkg/tmdb"
|
||||
"polaris/server/core"
|
||||
"polaris/ui"
|
||||
"time"
|
||||
|
||||
ginzap "github.com/gin-contrib/zap"
|
||||
|
||||
@@ -22,20 +24,24 @@ import (
|
||||
func NewServer(db *db.Client) *Server {
|
||||
r := gin.Default()
|
||||
s := &Server{
|
||||
r: r,
|
||||
db: db,
|
||||
language: db.GetLanguage(),
|
||||
r: r,
|
||||
db: db,
|
||||
language: db.GetLanguage(),
|
||||
monitorNumCache: cache.NewCache[int, int](10 * time.Minute),
|
||||
downloadNumCache: cache.NewCache[int, int](10 * time.Minute),
|
||||
}
|
||||
s.core = core.NewClient(db, s.language)
|
||||
return s
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
r *gin.Engine
|
||||
db *db.Client
|
||||
core *core.Client
|
||||
language string
|
||||
jwtSerect string
|
||||
r *gin.Engine
|
||||
db *db.Client
|
||||
core *core.Client
|
||||
language string
|
||||
jwtSerect string
|
||||
monitorNumCache *cache.Cache[int, int]
|
||||
downloadNumCache *cache.Cache[int, int]
|
||||
}
|
||||
|
||||
func (s *Server) Serve() error {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"polaris/log"
|
||||
"polaris/server/core"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pkg/errors"
|
||||
@@ -102,17 +103,25 @@ func (s *Server) GetTvWatchlist(c *gin.Context) (interface{}, error) {
|
||||
MonitoredNum: 0,
|
||||
DownloadedNum: 0,
|
||||
}
|
||||
|
||||
details := s.db.GetMediaDetails(item.ID)
|
||||
|
||||
for _, ep := range details.Episodes {
|
||||
if ep.Monitored {
|
||||
ms.MonitoredNum++
|
||||
if ep.Status == episode.StatusDownloaded {
|
||||
ms.DownloadedNum++
|
||||
mon, ok1 := s.monitorNumCache.Get(item.ID)
|
||||
dow, ok2 := s.downloadNumCache.Get(item.ID)
|
||||
if ok1 && ok2 {
|
||||
ms.MonitoredNum = mon
|
||||
ms.DownloadedNum = dow
|
||||
} else {
|
||||
details := s.db.GetMediaDetails(item.ID)
|
||||
for _, ep := range details.Episodes {
|
||||
if ep.Monitored {
|
||||
ms.MonitoredNum++
|
||||
if ep.Status == episode.StatusDownloaded {
|
||||
ms.DownloadedNum++
|
||||
}
|
||||
}
|
||||
}
|
||||
s.monitorNumCache.Set(item.ID, ms.MonitoredNum)
|
||||
s.downloadNumCache.Set(item.ID, ms.DownloadedNum)
|
||||
}
|
||||
|
||||
res[i] = ms
|
||||
}
|
||||
return res, nil
|
||||
@@ -162,9 +171,32 @@ func (s *Server) DeleteFromWatchlist(c *gin.Context) (interface{}, error) {
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "convert")
|
||||
}
|
||||
|
||||
deleteFiles := c.Query("delete_files")
|
||||
if strings.ToLower(deleteFiles) == "true" {
|
||||
//will delete local media file
|
||||
log.Infof("will delete local media files for %d", id)
|
||||
m, err := s.db.GetMedia(id)
|
||||
if err != nil {
|
||||
log.Warnf("get media: %v", err)
|
||||
} else {
|
||||
st, err := s.core.GetStorage(m.StorageID, m.MediaType)
|
||||
if err != nil {
|
||||
log.Warnf("get storage error: %v", err)
|
||||
} else {
|
||||
if err := st.RemoveAll(m.TargetDir); err != nil {
|
||||
log.Warnf("remove all : %v", err)
|
||||
} else {
|
||||
log.Infof("delete media files success: %v", m.TargetDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.db.DeleteMedia(id); err != nil {
|
||||
return nil, errors.Wrap(err, "delete db")
|
||||
}
|
||||
os.RemoveAll(filepath.Join(db.ImgPath, ids)) //delete image related
|
||||
|
||||
return "success", nil
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/date_symbol_data_local.dart';
|
||||
import 'package:ui/activity.dart';
|
||||
import 'package:ui/calendar.dart';
|
||||
import 'package:ui/init_wizard.dart';
|
||||
import 'package:ui/login_page.dart';
|
||||
import 'package:ui/movie_watchlist.dart';
|
||||
|
||||
@@ -24,9 +24,9 @@ class SeriesDetailData
|
||||
return SeriesDetails.fromJson(rsp.data);
|
||||
}
|
||||
|
||||
Future<void> delete() async {
|
||||
final dio = await APIs.getDio();
|
||||
var resp = await dio.delete("${APIs.seriesDetailUrl}$id");
|
||||
Future<void> delete(bool removeFiles ) async {
|
||||
final dio = APIs.getDio();
|
||||
var resp = await dio.delete("${APIs.seriesDetailUrl}$id", queryParameters: {"delete_files": removeFiles});
|
||||
var rsp = ServerResponse.fromJson(resp.data);
|
||||
if (rsp.code != 0) {
|
||||
throw rsp.message;
|
||||
|
||||
@@ -108,8 +108,8 @@ class SearchPageData
|
||||
"resolution": resolution,
|
||||
"folder": folder,
|
||||
"download_history_episodes": downloadHistoryEpisodes,
|
||||
"size_min": (limiter.start * 1000*1000).toInt(),
|
||||
"size_max": (limiter.end * 1000*1000).toInt(),
|
||||
"size_min": (limiter.start * 1000 * 1000).toInt(),
|
||||
"size_max": (limiter.end * 1000 * 1000).toInt(),
|
||||
});
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
if (sp.code != 0) {
|
||||
|
||||
@@ -44,14 +44,34 @@ class WelcomePageState extends ConsumerState<WelcomePage> {
|
||||
children: [
|
||||
() {
|
||||
return data.when(
|
||||
data: (value) => SingleChildScrollView(
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.start,
|
||||
spacing: isSmallScreen(context) ? 0 : 10,
|
||||
runSpacing: isSmallScreen(context) ? 10 : 20,
|
||||
children: getMediaAll(value),
|
||||
),
|
||||
),
|
||||
data: (value) {
|
||||
if (value.isEmpty) {
|
||||
return Container(
|
||||
height: MediaQuery.of(context).size.height * 0.7,
|
||||
alignment: Alignment.center,
|
||||
child: const Text(
|
||||
"啥都没有...",
|
||||
style: TextStyle(fontSize: 16),
|
||||
));
|
||||
}
|
||||
|
||||
if (onlyShowUnfinished) {
|
||||
value = value
|
||||
.where((v) => v.downloadedNum != v.monitoredNum)
|
||||
.toList();
|
||||
}
|
||||
|
||||
return GridView.builder(
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: cardWidth(context) ,
|
||||
childAspectRatio: 0.55,
|
||||
mainAxisSpacing: isSmallScreen(context) ? 10 : 20,
|
||||
crossAxisSpacing: isSmallScreen(context) ? 0 : 10),
|
||||
itemCount: value.length,
|
||||
itemBuilder: (context, index) =>
|
||||
MediaCard(item: value[index]),
|
||||
);
|
||||
},
|
||||
error: (err, trace) => PoNetworkError(err: err),
|
||||
loading: () => const MyProgressIndicator());
|
||||
}(),
|
||||
@@ -152,28 +172,6 @@ class WelcomePageState extends ConsumerState<WelcomePage> {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
return screenWidth < 600;
|
||||
}
|
||||
|
||||
List<Widget> getMediaAll(List<MediaDetail> list) {
|
||||
if (list.isEmpty) {
|
||||
return [
|
||||
Container(
|
||||
height: MediaQuery.of(context).size.height * 0.6,
|
||||
alignment: Alignment.center,
|
||||
child: const Text(
|
||||
"啥都没有...",
|
||||
style: TextStyle(fontSize: 16),
|
||||
))
|
||||
];
|
||||
}
|
||||
if (onlyShowUnfinished) {
|
||||
list = list.where((v) => v.downloadedNum != v.monitoredNum).toList();
|
||||
}
|
||||
return List.generate(list.length, (i) {
|
||||
final item = list[i];
|
||||
return MediaCard(item: item);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _showNameParsingDialog() async {
|
||||
final resultController = TextEditingController();
|
||||
return showDialog<void>(
|
||||
@@ -243,8 +241,6 @@ class WelcomePageState extends ConsumerState<WelcomePage> {
|
||||
|
||||
class MediaCard extends StatelessWidget {
|
||||
final MediaDetail item;
|
||||
static const double smallWidth = 110;
|
||||
static const double largeWidth = 140;
|
||||
|
||||
const MediaCard({super.key, required this.item});
|
||||
@override
|
||||
@@ -265,19 +261,18 @@ class MediaCard extends StatelessWidget {
|
||||
context.go(TvDetailsPage.toRoute(item.id!));
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
child: LayoutBuilder(builder: (context, constraints) => Wrap(
|
||||
direction: Axis.horizontal,
|
||||
children: <Widget>[
|
||||
Ink.image(
|
||||
width: constraints.maxWidth,
|
||||
height: constraints.maxWidth / 2 * 3,
|
||||
fit: BoxFit.cover,
|
||||
image: NetworkImage(
|
||||
"${APIs.imagesUrl}/${item.id}/poster_w500.jpg",
|
||||
)),
|
||||
SizedBox(
|
||||
width: cardWidth(context),
|
||||
height: cardWidth(context) / 2 * 3,
|
||||
child: Ink.image(
|
||||
fit: BoxFit.cover,
|
||||
image: NetworkImage(
|
||||
"${APIs.imagesUrl}/${item.id}/poster.jpg",
|
||||
)),
|
||||
),
|
||||
SizedBox(
|
||||
width: cardWidth(context),
|
||||
width: constraints.maxWidth,
|
||||
child: Column(
|
||||
children: [
|
||||
LinearProgressIndicator(
|
||||
@@ -297,15 +292,18 @@ class MediaCard extends StatelessWidget {
|
||||
],
|
||||
)),
|
||||
],
|
||||
),
|
||||
)),
|
||||
));
|
||||
}
|
||||
|
||||
double cardWidth(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
if (screenWidth < 600) {
|
||||
return smallWidth;
|
||||
}
|
||||
return largeWidth;
|
||||
}
|
||||
}
|
||||
|
||||
double cardWidth(BuildContext context) {
|
||||
const double smallWidth = 110;
|
||||
const double largeWidth = 140;
|
||||
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
if (screenWidth < 600) {
|
||||
return smallWidth;
|
||||
}
|
||||
return largeWidth;
|
||||
}
|
||||
|
||||
@@ -193,13 +193,25 @@ class _DetailCardState extends ConsumerState<DetailCard> {
|
||||
}
|
||||
|
||||
Future<void> showConfirmDialog(BuildContext oriContext) {
|
||||
var deleteFiles = false;
|
||||
return showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text("确认删除:"),
|
||||
content: Text("${widget.details.name}"),
|
||||
title: const Text("确认删除"),
|
||||
content: StatefulBuilder(builder: (context, setState) {
|
||||
return CheckboxListTile(
|
||||
value: deleteFiles,
|
||||
title: Text("删除媒体文件"),
|
||||
onChanged: (v) {
|
||||
setState(
|
||||
() {
|
||||
deleteFiles = v!;
|
||||
},
|
||||
);
|
||||
});
|
||||
}),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
@@ -209,7 +221,7 @@ class _DetailCardState extends ConsumerState<DetailCard> {
|
||||
ref
|
||||
.read(mediaDetailsProvider(widget.details.id.toString())
|
||||
.notifier)
|
||||
.delete()
|
||||
.delete(deleteFiles)
|
||||
.then((v) {
|
||||
if (oriContext.mounted) {
|
||||
oriContext.go(widget.details.mediaType == "tv"
|
||||
|
||||
@@ -53,10 +53,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.18.0"
|
||||
version: "1.19.0"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -217,18 +217,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
|
||||
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "10.0.5"
|
||||
version: "10.0.7"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
|
||||
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.5"
|
||||
version: "3.0.8"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -377,7 +377,7 @@ packages:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.99"
|
||||
version: "0.0.0"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -390,10 +390,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.11.1"
|
||||
version: "1.12.0"
|
||||
state_notifier:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -414,10 +414,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
|
||||
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.3.0"
|
||||
table_calendar:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -438,10 +438,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
|
||||
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.7.2"
|
||||
version: "0.7.3"
|
||||
timeago:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -534,10 +534,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
|
||||
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "14.2.5"
|
||||
version: "14.3.0"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
Reference in New Issue
Block a user