package db import ( "context" "encoding/json" "errors" "fmt" "pcgamedb/utils" "regexp" "strings" "time" "pcgamedb/cache" "pcgamedb/config" "pcgamedb/model" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) var ( removeNoneAlphaNumeric = regexp.MustCompile(`^[A-Za-z0-9]`) removeRepeatingSpacesRegex = regexp.MustCompile(`\s+`) ) func GetGameItemsByAuthor(regex string) ([]*model.GameItem, error) { var res []*model.GameItem ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() filter := bson.D{{Key: "author", Value: primitive.Regex{Pattern: regex, Options: "i"}}} cursor, err := GameItemCollection.Find(ctx, filter) if err != nil { return nil, err } defer func(cursor *mongo.Cursor, ctx context.Context) { _ = cursor.Close(ctx) }(cursor, ctx) if cursor.Err() != nil { return nil, cursor.Err() } if err = cursor.All(ctx, &res); err != nil { return nil, err } return res, err } func GetGameItemsByAuthorPagination(regex string, page int, pageSize int) ([]*model.GameItem, int, error) { var res []*model.GameItem ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() filter := bson.D{{Key: "author", Value: primitive.Regex{Pattern: regex, Options: "i"}}} opts := options.Find() opts.SetSkip(int64((page - 1) * pageSize)) opts.SetLimit(int64(pageSize)) totalCount, err := GameItemCollection.CountDocuments(ctx, filter) if err != nil { return nil, 0, err } totalPage := (totalCount + int64(pageSize) - 1) / int64(pageSize) cursor, err := GameItemCollection.Find(ctx, filter, opts) if err != nil { return nil, 0, err } defer func(cursor *mongo.Cursor, ctx context.Context) { _ = cursor.Close(ctx) }(cursor, ctx) if cursor.Err() != nil { return nil, 0, cursor.Err() } if err = cursor.All(ctx, &res); err != nil { return nil, 0, err } return res, int(totalPage), err } func IsGameCrawled(flag string, author string) bool { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() filter := bson.D{ {Key: "author", Value: primitive.Regex{Pattern: author, Options: "i"}}, {Key: "update_flag", Value: flag}, } var game model.GameItem err := GameItemCollection.FindOne(ctx, filter).Decode(&game) if err != nil { if errors.Is(err, mongo.ErrNoDocuments) { return false } return false } return true } func IsGameCrawledByURL(url string) bool { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() filter := bson.D{ {Key: "url", Value: url}, } var game model.GameItem err := GameItemCollection.FindOne(ctx, filter).Decode(&game) if err != nil { if errors.Is(err, mongo.ErrNoDocuments) { return false } return false } return true } func SaveGameItem(item *model.GameItem) error { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if item.ID.IsZero() { item.ID = primitive.NewObjectID() } if item.CreatedAt.IsZero() { item.CreatedAt = time.Now() } item.UpdatedAt = time.Now() item.Size = strings.Replace(item.Size, "gb", "GB", -1) item.Size = strings.Replace(item.Size, "mb", "MB", -1) filter := bson.M{"_id": item.ID} update := bson.M{"$set": item} opts := options.Update().SetUpsert(true) _, err := GameItemCollection.UpdateOne(ctx, filter, update, opts) if err != nil { return err } return nil } func SaveGameItems(items []*model.GameItem) error { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() operations := make([]mongo.WriteModel, len(items)) for i, item := range items { if item.ID.IsZero() { item.ID = primitive.NewObjectID() } if item.CreatedAt.IsZero() { item.CreatedAt = time.Now() } item.UpdatedAt = time.Now() item.Size = strings.Replace(item.Size, "gb", "GB", -1) item.Size = strings.Replace(item.Size, "mb", "MB", -1) operations[i] = mongo.NewUpdateOneModel(). SetFilter(bson.D{{Key: "_id", Value: item.ID}}). SetUpdate(bson.D{{Key: "$set", Value: item}}). SetUpsert(true) } opts := options.BulkWrite().SetOrdered(false) _, err := GameItemCollection.BulkWrite(ctx, operations, opts) if err != nil { return err } return nil } func SaveGameInfo(item *model.GameInfo) error { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if item.ID.IsZero() { item.ID = primitive.NewObjectID() } if item.CreatedAt.IsZero() { item.CreatedAt = time.Now() } if item.InfoUpdatedAt.IsZero() { item.InfoUpdatedAt = item.CreatedAt } item.UpdatedAt = time.Now() filter := bson.M{"_id": item.ID} update := bson.M{"$set": item} opts := options.Update().SetUpsert(true) _, err := GameInfoCollection.UpdateOne(ctx, filter, update, opts) if err != nil { return err } return nil } func SaveGameInfos(items []*model.GameInfo) error { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() operations := make([]mongo.WriteModel, len(items)) for i, item := range items { if item.ID.IsZero() { item.ID = primitive.NewObjectID() } if item.CreatedAt.IsZero() { item.CreatedAt = time.Now() } if item.InfoUpdatedAt.IsZero() { item.InfoUpdatedAt = item.CreatedAt } item.UpdatedAt = time.Now() operations[i] = mongo.NewUpdateOneModel(). SetFilter(bson.D{{Key: "_id", Value: item.ID}}). SetUpdate(bson.D{{Key: "$set", Value: item}}). SetUpsert(true) } opts := options.BulkWrite().SetOrdered(false) _, err := GameInfoCollection.BulkWrite(ctx, operations, opts) if err != nil { return err } return nil } func GetAllGameItems() ([]*model.GameItem, error) { var items []*model.GameItem ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() cursor, err := GameItemCollection.Find(ctx, bson.D{}) if err != nil { return nil, err } defer func(cursor *mongo.Cursor, ctx context.Context) { _ = cursor.Close(ctx) }(cursor, ctx) for cursor.Next(ctx) { var game model.GameItem if err = cursor.Decode(&game); err != nil { return nil, err } items = append(items, &game) } if cursor.Err() != nil { return nil, cursor.Err() } return items, err } func GetGameItemByUrl(url string) (*model.GameItem, error) { var item model.GameItem ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() filter := bson.M{"url": url} err := GameItemCollection.FindOne(ctx, filter).Decode(&item) if err != nil { if errors.Is(err, mongo.ErrNoDocuments) { return &model.GameItem{}, nil } return nil, err } return &item, nil } func GetGameItemByID(id primitive.ObjectID) (*model.GameItem, error) { var item model.GameItem ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() filter := bson.M{"_id": id} err := GameItemCollection.FindOne(ctx, filter).Decode(&item) if err != nil { return nil, err } return &item, nil } func GetGameItemsByIDs(ids []primitive.ObjectID) ([]*model.GameItem, error) { if len(ids) == 0 { return nil, nil } var items []*model.GameItem ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() cursor, err := GameItemCollection.Find(ctx, bson.M{"_id": bson.M{"$in": ids}}) if err != nil { return nil, err } defer func(cursor *mongo.Cursor, ctx context.Context) { _ = cursor.Close(ctx) }(cursor, ctx) for cursor.Next(ctx) { var game model.GameItem if err = cursor.Decode(&game); err != nil { return nil, err } items = append(items, &game) } if cursor.Err() != nil { return nil, cursor.Err() } return items, err } func SearchGameInfos(name string, page int, pageSize int) ([]*model.GameInfo, int, error) { var items []*model.GameInfo name = removeNoneAlphaNumeric.ReplaceAllString(name, " ") name = removeRepeatingSpacesRegex.ReplaceAllString(name, " ") name = strings.TrimSpace(name) name = strings.Replace(name, " ", ".*", -1) name = fmt.Sprintf("%s.*", name) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() filter := bson.M{"$or": []interface{}{ bson.M{"name": bson.M{"$regex": primitive.Regex{Pattern: name, Options: "i"}}}, bson.M{"aliases": bson.M{"$regex": primitive.Regex{Pattern: name, Options: "i"}}}, }} totalCount, err := GameInfoCollection.CountDocuments(ctx, filter) if err != nil { return nil, 0, err } totalPage := (totalCount + int64(pageSize) - 1) / int64(pageSize) findOpts := options.Find().SetSkip(int64((page - 1) * pageSize)).SetLimit(int64(pageSize)).SetSort(bson.D{{Key: "name", Value: 1}}) cursor, err := GameInfoCollection.Find(ctx, filter, findOpts) if err != nil { return nil, 0, err } defer func(cursor *mongo.Cursor, ctx context.Context) { _ = cursor.Close(ctx) }(cursor, ctx) for cursor.Next(ctx) { var game model.GameInfo if err = cursor.Decode(&game); err != nil { return nil, 0, err } game.Games, err = GetGameItemsByIDs(game.GameIDs) if err != nil { return nil, 0, err } items = append(items, &game) } if err := cursor.Err(); err != nil { return nil, 0, err } return items, int(totalPage), nil } func SearchGameInfosCache(name string, page int, pageSize int) ([]*model.GameInfo, int, error) { type res struct { Items []*model.GameInfo TotalPage int } name = strings.ToLower(name) if config.Config.RedisAvaliable { key := fmt.Sprintf("searchGameDetails:%s:%d:%d", name, page, pageSize) val, exist := cache.Get(key) if exist { var data res err := json.Unmarshal([]byte(val), &data) if err != nil { return nil, 0, err } return data.Items, data.TotalPage, nil } else { data, totalPage, err := SearchGameInfos(name, page, pageSize) if err != nil { return nil, 0, err } dataBytes, err := json.Marshal(res{Items: data, TotalPage: totalPage}) if err != nil { return nil, 0, err } _ = cache.AddWithExpire(key, string(dataBytes), 5*time.Minute) return data, totalPage, nil } } else { return SearchGameInfos(name, page, pageSize) } } func GetGameInfoByPlatformID(platform string, id int) (*model.GameInfo, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() var filter interface{} switch platform { case "steam": filter = bson.M{"steam_id": id} case "igdb": filter = bson.M{"igdb_id": id} } var game model.GameInfo err := GameInfoCollection.FindOne(ctx, filter).Decode(&game) if err != nil { return nil, err } return &game, nil } func GetGameInfosByPlatformIDs(platform string, ids []int) ([]*model.GameInfo, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() var filter interface{} switch platform { case "steam": filter = bson.M{"steam_id": bson.M{"$in": ids}} case "igdb": filter = bson.M{"igdb_id": bson.M{"$in": ids}} } var games []*model.GameInfo cursor, err := GameInfoCollection.Find(ctx, filter) if err != nil { return nil, err } if err = cursor.All(ctx, &games); err != nil { return nil, err } return games, nil } func HasGameItemOrganized(id primitive.ObjectID) (bool, []*model.GameInfo) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() filter := bson.M{"games": id} var res []*model.GameInfo cursor, err := GameInfoCollection.Find(ctx, filter) if err != nil { return false, nil } if err = cursor.All(ctx, &res); err != nil { return false, nil } if len(res) == 0 { return false, nil } return true, res } func GetUnorganizedGameItems(num int) ([]*model.GameItem, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() var gamesNotInDetails []*model.GameItem pipeline := mongo.Pipeline{ bson.D{{Key: "$lookup", Value: bson.D{ {Key: "from", Value: gameInfoCollectionName}, {Key: "localField", Value: "_id"}, {Key: "foreignField", Value: "games"}, {Key: "as", Value: "gameDetail"}, }}}, } if num != -1 && num > 0 { pipeline = append(pipeline, bson.D{{Key: "$limit", Value: num}}) } pipeline = append(pipeline, bson.D{{Key: "$match", Value: bson.D{ {Key: "gameDetail", Value: bson.D{{Key: "$size", Value: 0}}}, }}}, bson.D{{Key: "$sort", Value: bson.D{{Key: "name", Value: 1}}}}, ) cursor, err := GameItemCollection.Aggregate(ctx, pipeline) if err != nil { return nil, err } defer func(cursor *mongo.Cursor, ctx context.Context) { _ = cursor.Close(ctx) }(cursor, ctx) for cursor.Next(ctx) { var game model.GameItem if err := cursor.Decode(&game); err != nil { return nil, err } gamesNotInDetails = append(gamesNotInDetails, &game) } if err := cursor.Err(); err != nil { return nil, err } return gamesNotInDetails, nil } func GetGameInfoByID(id primitive.ObjectID) (*model.GameInfo, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() var game model.GameInfo err := GameInfoCollection.FindOne(ctx, bson.M{"_id": id}).Decode(&game) if err != nil { return nil, err } return &game, nil } func DeduplicateGameItems() ([]primitive.ObjectID, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() type queryRes struct { ID struct { RawName string `bson:"raw_name"` Download string `bson:"download"` } `bson:"_id"` Count int `bson:"count"` IDs []primitive.ObjectID `bson:"ids"` } var res []primitive.ObjectID pipeline := mongo.Pipeline{ bson.D{ {Key: "$group", Value: bson.D{ {Key: "_id", Value: bson.D{ {Key: "raw_name", Value: "$raw_name"}, {Key: "download", Value: "$download"}, }, }, {Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}, {Key: "ids", Value: bson.D{{Key: "$push", Value: "$_id"}}}, }, }, }, bson.D{{Key: "$match", Value: bson.D{{Key: "count", Value: bson.D{{Key: "$gt", Value: 1}}}}}}, } var qres []queryRes cursor, err := GameItemCollection.Aggregate(ctx, pipeline) if err != nil { return nil, err } if err = cursor.All(ctx, &qres); err != nil { return nil, err } for _, item := range qres { res = append(res, item.IDs[1:]...) } err = DeleteGameItemsByIDs(res) if err != nil { return nil, err } _, _ = CleanOrphanGamesInGameInfos() return res, nil } func CleanOrphanGamesInGameInfos() (map[primitive.ObjectID]primitive.ObjectID, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() pipeline := mongo.Pipeline{ bson.D{{Key: "$unwind", Value: "$games"}}, bson.D{{Key: "$lookup", Value: bson.D{ {Key: "from", Value: gameItemCollectionName}, {Key: "localField", Value: "games"}, {Key: "foreignField", Value: "_id"}, {Key: "as", Value: "gameDownloads"}, }}}, bson.D{{Key: "$match", Value: bson.D{ {Key: "gameDownloads", Value: bson.D{{Key: "$size", Value: 0}}}, }}}, bson.D{{Key: "$project", Value: bson.D{ {Key: "_id", Value: 1}, {Key: "game", Value: "$games"}, }}}, } cursor, err := GameInfoCollection.Aggregate(ctx, pipeline) if err != nil { return nil, err } qres := make([]struct { ID primitive.ObjectID `bson:"_id"` Game primitive.ObjectID `bson:"game"` }, 0) if err := cursor.All(ctx, &qres); err != nil { return nil, err } var res = make(map[primitive.ObjectID]primitive.ObjectID) for _, item := range qres { info, err := GetGameInfoByID(item.ID) if err != nil { continue } newGames := make([]primitive.ObjectID, 0, len(info.GameIDs)) for _, id := range info.GameIDs { if id != item.Game { newGames = append(newGames, id) } } info.GameIDs = newGames if err := SaveGameInfo(info); err != nil { return nil, err } res[item.ID] = item.Game } return res, nil } func CleanGameInfoWithEmptyGameIDs() ([]primitive.ObjectID, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() filter := bson.M{"games": bson.M{"$size": 0}} cursor, err := GameInfoCollection.Find(ctx, filter) if err != nil { return nil, err } var games []*model.GameInfo var res []primitive.ObjectID if err = cursor.All(ctx, &games); err != nil { return nil, err } for _, item := range games { res = append(res, item.ID) } _, err = GameInfoCollection.DeleteMany(ctx, filter) if err != nil { return nil, err } return res, nil } func GetGameInfosByName(name string) ([]*model.GameInfo, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() name = strings.TrimSpace(name) name = fmt.Sprintf("^%s$", name) filter := bson.M{"name": bson.M{"$regex": primitive.Regex{Pattern: name, Options: "i"}}} cursor, err := GameInfoCollection.Find(ctx, filter) if err != nil { return nil, err } var games []*model.GameInfo if err = cursor.All(ctx, &games); err != nil { return nil, err } return games, nil } func GetGameItemByRawName(name string) ([]*model.GameItem, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() name = strings.TrimSpace(name) name = fmt.Sprintf("^%s$", name) filter := bson.M{"raw_name": bson.M{"$regex": primitive.Regex{Pattern: name, Options: "i"}}} cursor, err := GameItemCollection.Find(ctx, filter) if err != nil { return nil, err } var game []*model.GameItem if err = cursor.All(ctx, &game); err != nil { return nil, err } return game, nil } func GetSameNameGameInfos() (map[string][]primitive.ObjectID, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() pipeline := mongo.Pipeline{ bson.D{{Key: "$group", Value: bson.D{ {Key: "_id", Value: "$name"}, {Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}, {Key: "ids", Value: bson.D{{Key: "$addToSet", Value: "$_id"}}}, }}}, bson.D{{Key: "$match", Value: bson.D{{Key: "count", Value: bson.D{{Key: "$gt", Value: 1}}}}}}, } cursor, err := GameInfoCollection.Aggregate(ctx, pipeline) if err != nil { return nil, err } data := make([]struct { Name string `bson:"_id"` Count int `bson:"count"` IDs []primitive.ObjectID `bson:"ids"` }, 0) if err := cursor.All(ctx, &data); err != nil { return nil, err } res := make(map[string][]primitive.ObjectID) for _, item := range data { res[item.Name] = item.IDs } return res, nil } func DeleteGameInfosByIDs(ids []primitive.ObjectID) error { if len(ids) == 0 { return nil } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() _, err := GameInfoCollection.DeleteMany(ctx, bson.M{"_id": bson.M{"$in": ids}}) if err != nil { return err } return nil } func MergeGameInfosWithSameIGDBID() error { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() pipeline := mongo.Pipeline{ bson.D{{Key: "$group", Value: bson.D{ {Key: "_id", Value: "$igdb_id"}, {Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}, {Key: "docs", Value: bson.D{{Key: "$push", Value: "$$ROOT"}}}, }}}, bson.D{{Key: "$match", Value: bson.D{{Key: "count", Value: bson.D{{Key: "$gt", Value: 1}}}}}}, } cursor, err := GameInfoCollection.Aggregate(ctx, pipeline) if err != nil { return err } type queryRes struct { IGDBID int `bson:"_id"` Infos []model.GameInfo `bson:"docs"` } var res []queryRes if err = cursor.All(ctx, &res); err != nil { return err } for _, item := range res { gameIDs := make([]primitive.ObjectID, 0) deleteInfoIDs := make([]primitive.ObjectID, 0) for _, info := range item.Infos[1:] { gameIDs = append(gameIDs, info.GameIDs...) deleteInfoIDs = append(deleteInfoIDs, info.ID) } item.Infos[0].GameIDs = utils.Unique(append(item.Infos[0].GameIDs, gameIDs...)) err = DeleteGameInfosByIDs(deleteInfoIDs) if err != nil { return err } } return nil } func MergeGameInfosWithSameName() error { games, err := GetSameNameGameInfos() if err != nil { return err } for _, ids := range games { var IGDBItem *model.GameInfo = nil otherPlatformItems := make([]*model.GameInfo, 0) skip := false for _, id := range ids { item, err := GetGameInfoByID(id) if err != nil { continue } if item.IGDBID != 0 { if IGDBItem == nil { IGDBItem = item } else { skip = true break // skip if there are multiple items with IGDB ID // not sure which item is correct // need deal manually } } else { otherPlatformItems = append(otherPlatformItems, item) } } if skip { continue } if IGDBItem != nil { for _, item := range otherPlatformItems { IGDBItem.GameIDs = append(IGDBItem.GameIDs, item.ID) } if err := SaveGameInfo(IGDBItem); err != nil { continue } } } return nil } func GetGameInfoCount() (int64, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() count, err := GameInfoCollection.CountDocuments(ctx, bson.M{}) if err != nil { return 0, err } return count, nil } func GetGameItemCount() (int64, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() count, err := GameItemCollection.CountDocuments(ctx, bson.M{}) if err != nil { return 0, err } return count, nil } func GetGameInfoWithSteamID() ([]*model.GameInfo, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() filter := bson.M{"$and": []bson.M{ {"steam_id": bson.M{"$exists": 1}}, {"steam_id": bson.M{"$ne": 0}}, }} cursor, err := GameInfoCollection.Find(ctx, filter) if err != nil { return nil, err } var games []*model.GameInfo if err = cursor.All(ctx, &games); err != nil { return nil, err } return games, nil } func DeleteGameInfoByID(id primitive.ObjectID) error { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() _, err := GameInfoCollection.DeleteOne(ctx, bson.M{"_id": id}) if err != nil { return err } return nil } func DeleteGameItemByID(id primitive.ObjectID) error { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() _, err := GameItemCollection.DeleteOne(ctx, bson.M{"_id": id}) if err != nil { return err } filter := bson.M{"games": bson.M{"$in": []primitive.ObjectID{id}}} cursor, err := GameInfoCollection.Find(ctx, filter) if err != nil { return err } var games []*model.GameInfo if err = cursor.All(ctx, &games); err != nil { return err } for _, game := range games { newIDs := make([]primitive.ObjectID, 0) for _, gameID := range game.GameIDs { if gameID != id { newIDs = append(newIDs, gameID) } } game.GameIDs = newIDs if err := SaveGameInfo(game); err != nil { continue } } return nil } func DeleteGameItemsByIDs(ids []primitive.ObjectID) error { if len(ids) == 0 { return nil } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() _, err := GameItemCollection.DeleteMany(ctx, bson.M{"_id": bson.M{"$in": ids}}) if err != nil { return err } update := bson.D{{Key: "$pull", Value: bson.D{ {Key: "games", Value: bson.D{ {Key: "$in", Value: ids}, }}, }}} _, err = GameInfoCollection.UpdateMany(ctx, bson.M{}, update) if err != nil { return err } return nil } func GetAllAuthors() ([]string, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() pipeline := mongo.Pipeline{ bson.D{{Key: "$group", Value: bson.D{ {Key: "_id", Value: "$author"}, }}}, } cursor, err := GameItemCollection.Aggregate(ctx, pipeline) if err != nil { return nil, err } var authors []struct { Author string `bson:"_id"` } if err = cursor.All(ctx, &authors); err != nil { return nil, err } var res []string for _, author := range authors { res = append(res, author.Author) } return res, nil } func GetAllGameInfos() ([]*model.GameInfo, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() cursor, err := GameInfoCollection.Find(ctx, bson.M{}) if err != nil { return nil, err } var res []*model.GameInfo if err = cursor.All(ctx, &res); err != nil { return nil, err } return res, nil } func GetGameInfoByGameItemID(id primitive.ObjectID) (*model.GameInfo, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() filter := bson.M{"games": id} cursor, err := GameInfoCollection.Find(ctx, filter) if err != nil { return nil, err } var res []*model.GameInfo if err = cursor.All(ctx, &res); err != nil { return nil, err } if len(res) == 0 { return nil, errors.New("game info not found") } return res[0], nil } func GetOutdatedGameInfos(maxNum int) ([]*model.GameInfo, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() opts := options.Find().SetLimit(int64(maxNum)) filter := bson.M{ "info_updated_at": bson.M{"$lt": time.Now().Add(-24 * time.Hour * 30)}, } cursor, err := GameInfoCollection.Find(ctx, filter, opts) if err != nil { return nil, err } var res []*model.GameInfo if err = cursor.All(ctx, &res); err != nil { return nil, err } return res, nil } func MergeGameInfo(oldInfo *model.GameInfo, newInfo *model.GameInfo) { newInfo.ID = oldInfo.ID newInfo.UpdatedAt = time.Now() newInfo.GameIDs = oldInfo.GameIDs newInfo.IGDBID = oldInfo.IGDBID newInfo.SteamID = oldInfo.SteamID newInfo.CreatedAt = oldInfo.CreatedAt newInfo.InfoUpdatedAt = time.Now() }