generated from ricardo/MVCLogin
Compare commits
3 Commits
4cca23cb35
...
058b38a664
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
058b38a664 | ||
|
|
fa8aa0b610 | ||
|
|
a32c3c9eb9 |
@ -29,5 +29,6 @@ namespace BaseDomain.Results
|
|||||||
|
|
||||||
public static Result Success() => new(true, Error.None);
|
public static Result Success() => new(true, Error.None);
|
||||||
public static Result Failure(Error error) => new(false, Error.None);
|
public static Result Failure(Error error) => new(false, Error.None);
|
||||||
|
public static Result Failure<T>(Error error) => new(false, Error.None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,5 +30,9 @@ namespace BaseDomain.Results
|
|||||||
return new Result<TValue>(value, true, Error.None);
|
return new Result<TValue>(value, true, Error.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Result<TValue> Failure(string v)
|
||||||
|
{
|
||||||
|
return new Result<TValue>(default, false, new Error(ErrorTypeEnum.Failure, v));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,7 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Postall.Domain
|
namespace Postall.Domain.Contracts.Repositories
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Interface para repositório de canais do YouTube no MongoDB
|
/// Interface para repositório de canais do YouTube no MongoDB
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
using MongoDB.Driver;
|
||||||
|
using Postall.Domain.Entities;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Postall.Domain
|
||||||
|
{
|
||||||
|
public interface IUserSocialRepository
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Obtém todos os dados sociais dos usuários
|
||||||
|
/// </summary>
|
||||||
|
Task<IEnumerable<UserSocialData>> GetAllAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtém os dados sociais pelo ID do MongoDB
|
||||||
|
/// </summary>
|
||||||
|
Task<UserSocialData> GetByIdAsync(string id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtém os dados sociais pelo ID do usuário
|
||||||
|
/// </summary>
|
||||||
|
Task<UserSocialData> GetByUserIdAsync(string userId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtém os dados sociais pelo token do Google
|
||||||
|
/// </summary>
|
||||||
|
Task<UserSocialData> GetByGoogleTokenAsync(string googleToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adiciona novos dados sociais de usuário
|
||||||
|
/// </summary>
|
||||||
|
Task<UserSocialData> AddAsync(UserSocialData userSocialData);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Atualiza os dados sociais de um usuário existente
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> UpdateAsync(UserSocialData userSocialData);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Atualiza ou insere os dados sociais de um usuário
|
||||||
|
/// </summary>
|
||||||
|
Task<UserSocialData> UpsertAsync(UserSocialData userSocialData);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Atualiza ou insere os dados sociais de um usuário unico pelo id
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> UpdateOneAsync(string userId, UpdateDefinition<UserSocialData> update);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove os dados sociais pelo ID do MongoDB
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> DeleteAsync(string id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove os dados sociais pelo ID do usuário
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> DeleteByUserIdAsync(string userId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Atualiza apenas o token do Google para um usuário
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> UpdateGoogleTokenAsync(string userId, string googleToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Atualiza apenas o token do Facebook para um usuário
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> UpdateFacebookTokenAsync(string userId, FacebookToken facebookToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
Postall.Domain/Contracts/Repositories/IVideoRepository.cs
Normal file
23
Postall.Domain/Contracts/Repositories/IVideoRepository.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
using Postall.Domain.Entities;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Postall.Domain
|
||||||
|
{
|
||||||
|
public interface IVideoRepository
|
||||||
|
{
|
||||||
|
Task<IEnumerable<VideoData>> GetAllAsync();
|
||||||
|
Task<VideoData> GetByIdAsync(string id);
|
||||||
|
Task<VideoData> GetByVideoIdAsync(string videoId);
|
||||||
|
Task<IList<VideoData>> GetByUserIdAsync(string userId);
|
||||||
|
Task<IList<VideoData>> GetByChannelIdAsync(string channelId);
|
||||||
|
Task<IList<VideoData>> GetByUserIdAndChannelIdAsync(string userId, string channelId);
|
||||||
|
Task<VideoData> GetByUserIdAndVideoIdAsync(string userId, string videoId);
|
||||||
|
Task<VideoData> AddAsync(VideoData videoData);
|
||||||
|
Task<IEnumerable<VideoData>> AddManyAsync(IEnumerable<VideoData> videos);
|
||||||
|
Task<bool> UpdateAsync(VideoData videoData);
|
||||||
|
Task<bool> DeleteAsync(string id);
|
||||||
|
Task<bool> DeleteByVideoIdAsync(string videoId);
|
||||||
|
Task<IEnumerable<VideoData>> SearchAsync(string searchTerm);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,6 +11,7 @@ namespace Postall.Domain.Dtos
|
|||||||
{
|
{
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
public string UserId { get; set; }
|
public string UserId { get; set; }
|
||||||
|
public string ChannelId { get; set; }
|
||||||
public string YoutubeId { get; set; }
|
public string YoutubeId { get; set; }
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
public string Description { get; set; }
|
public string Description { get; set; }
|
||||||
@ -23,15 +24,16 @@ namespace Postall.Domain.Dtos
|
|||||||
public bool IsSelected { get; set; }
|
public bool IsSelected { get; set; }
|
||||||
|
|
||||||
// URL do canal no YouTube
|
// URL do canal no YouTube
|
||||||
public string ChannelUrl => $"https://www.youtube.com/channel/{Id}";
|
public string ChannelUrl => $"https://www.youtube.com/channel/{this.ChannelId}";
|
||||||
|
|
||||||
public ChannelData ToChannelData()
|
public ChannelData ToChannelData()
|
||||||
{
|
{
|
||||||
return new ChannelData
|
return new ChannelData
|
||||||
{
|
{
|
||||||
Id = Id,
|
Id = Guid.NewGuid().ToString("N"),
|
||||||
UserId = UserId,
|
UserId = this.UserId,
|
||||||
YoutubeId = YoutubeId,
|
ChannelId = this.ChannelId,
|
||||||
|
YoutubeId = this.YoutubeId,
|
||||||
Title = Title,
|
Title = Title,
|
||||||
Description = Description,
|
Description = Description,
|
||||||
ThumbnailUrl = ThumbnailUrl,
|
ThumbnailUrl = ThumbnailUrl,
|
||||||
|
|||||||
65
Postall.Domain/Dtos/VideoResponse.cs
Normal file
65
Postall.Domain/Dtos/VideoResponse.cs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
using Postall.Domain.Entities;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Postall.Domain.Dtos
|
||||||
|
{
|
||||||
|
public class VideoResponse
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string VideoId { get; set; }
|
||||||
|
public string ChannelId { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
public string ThumbnailUrl { get; set; }
|
||||||
|
public DateTime PublishedAt { get; set; }
|
||||||
|
public ulong ViewCount { get; set; }
|
||||||
|
public ulong LikeCount { get; set; }
|
||||||
|
public ulong DislikeCount { get; set; }
|
||||||
|
public ulong CommentCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class VideoDataExtensions
|
||||||
|
{
|
||||||
|
public static VideoResponse ToVideoResponse(this VideoData videoData)
|
||||||
|
{
|
||||||
|
if (videoData == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return new VideoResponse
|
||||||
|
{
|
||||||
|
Id = videoData.Id,
|
||||||
|
VideoId = videoData.VideoId,
|
||||||
|
ChannelId = videoData.ChannelId,
|
||||||
|
Title = videoData.Title,
|
||||||
|
Description = videoData.Description,
|
||||||
|
ThumbnailUrl = videoData.ThumbnailUrl,
|
||||||
|
PublishedAt = videoData.PublishedAt,
|
||||||
|
ViewCount = videoData.ViewCount,
|
||||||
|
LikeCount = videoData.LikeCount,
|
||||||
|
DislikeCount = videoData.DislikeCount,
|
||||||
|
CommentCount = videoData.CommentCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static VideoData ToVideoData(this VideoResponse videoResponse)
|
||||||
|
{
|
||||||
|
if (videoResponse == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return new VideoData
|
||||||
|
{
|
||||||
|
Id = videoResponse.Id,
|
||||||
|
VideoId = videoResponse.VideoId,
|
||||||
|
ChannelId = videoResponse.ChannelId,
|
||||||
|
Title = videoResponse.Title,
|
||||||
|
Description = videoResponse.Description,
|
||||||
|
ThumbnailUrl = videoResponse.ThumbnailUrl,
|
||||||
|
PublishedAt = videoResponse.PublishedAt,
|
||||||
|
ViewCount = videoResponse.ViewCount,
|
||||||
|
LikeCount = videoResponse.LikeCount,
|
||||||
|
DislikeCount = videoResponse.DislikeCount,
|
||||||
|
CommentCount = videoResponse.CommentCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -49,13 +49,14 @@ namespace Postall.Domain.Entities
|
|||||||
public bool IsSelected { get; set; }
|
public bool IsSelected { get; set; }
|
||||||
|
|
||||||
[BsonIgnore]
|
[BsonIgnore]
|
||||||
public string ChannelUrl => $"https://www.youtube.com/channel/{YoutubeId}";
|
public string ChannelUrl => $"https://www.youtube.com/channel/{ChannelId}";
|
||||||
|
|
||||||
public ChannelResponse ToChannelResponse() => new ChannelResponse
|
public ChannelResponse ToChannelResponse() => new ChannelResponse
|
||||||
{
|
{
|
||||||
Id = Id,
|
Id = Id,
|
||||||
UserId = UserId,
|
UserId = UserId,
|
||||||
YoutubeId = YoutubeId,
|
YoutubeId = YoutubeId,
|
||||||
|
ChannelId = ChannelId,
|
||||||
Title = Title,
|
Title = Title,
|
||||||
Description = Description,
|
Description = Description,
|
||||||
ThumbnailUrl = ThumbnailUrl,
|
ThumbnailUrl = ThumbnailUrl,
|
||||||
|
|||||||
27
Postall.Domain/Entities/VideosData.cs
Normal file
27
Postall.Domain/Entities/VideosData.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
using System;
|
||||||
|
using MongoDB.Bson;
|
||||||
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
|
|
||||||
|
namespace Postall.Domain.Entities
|
||||||
|
{
|
||||||
|
public class VideoData
|
||||||
|
{
|
||||||
|
[BsonId]
|
||||||
|
[BsonRepresentation(BsonType.ObjectId)]
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
public string UserId { get; set; }
|
||||||
|
public string ChannelId { get; set; }
|
||||||
|
public string VideoId { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
public string ThumbnailUrl { get; set; }
|
||||||
|
public DateTime PublishedAt { get; set; }
|
||||||
|
public ulong ViewCount { get; set; }
|
||||||
|
public ulong LikeCount { get; set; }
|
||||||
|
public ulong DislikeCount { get; set; }
|
||||||
|
public ulong CommentCount { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using BaseDomain.Results;
|
using BaseDomain.Results;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Postall.Domain;
|
using Postall.Domain.Contracts.Repositories;
|
||||||
using Postall.Domain.Dtos;
|
using Postall.Domain.Dtos;
|
||||||
using Postall.Domain.Entities;
|
using Postall.Domain.Entities;
|
||||||
using Postall.Domain.Services.Contracts;
|
using Postall.Domain.Services.Contracts;
|
||||||
@ -114,8 +114,11 @@ namespace Postall.Infra.Services
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
var channelDetails = await GetChannelDetailsAsync(channelId);
|
var channelDetails = await GetChannelDetailsAsync(channelId);
|
||||||
|
var data = channelDetails.Value.ToChannelData();
|
||||||
await _channelRepository.AddAsync(channelDetails.Value.ToChannelData());
|
data.ChannelId = channelId;
|
||||||
|
data.UserId = userId;
|
||||||
|
data.YoutubeId = channelId;
|
||||||
|
await _channelRepository.AddAsync(data);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace Postall.Domain.Services.Contracts
|
namespace Postall.Domain.Services.Contracts
|
||||||
{
|
{
|
||||||
public interface IVideoService
|
public interface IYouTubeServiceChannel
|
||||||
{
|
{
|
||||||
Task<List<VideoItemResponse>> GetUserChannelVideosAsync(int maxResults = 10);
|
Task<List<VideoItemResponse>> GetUserChannelVideosAsync(int maxResults = 10);
|
||||||
|
|
||||||
|
|||||||
14
Postall.Domain/Services/Contracts/IVideoYoutubeService.cs
Normal file
14
Postall.Domain/Services/Contracts/IVideoYoutubeService.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using BaseDomain.Results;
|
||||||
|
using Postall.Domain.Dtos;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Postall.Domain.Services.Contracts
|
||||||
|
{
|
||||||
|
public interface IVideoYoutubeService
|
||||||
|
{
|
||||||
|
Task<Result<VideoResponse>> GetVideoDetailsAsync(string videoId);
|
||||||
|
Task<Result<List<VideoResponse>>> GetChannelVideosAsync(string channelId, int maxResults = 50);
|
||||||
|
Task<Result<List<VideoResponse>>> SearchVideosAsync(string query, int maxResults = 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
Postall.Domain/Services/Contracts/IYouTubeServiceChannel.cs
Normal file
19
Postall.Domain/Services/Contracts/IYouTubeServiceChannel.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
using BaseDomain.Results;
|
||||||
|
using Postall.Domain.Dtos;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Postall.Domain.Services.Contracts
|
||||||
|
{
|
||||||
|
public interface IVideoService
|
||||||
|
{
|
||||||
|
Task<Result<List<VideoResponse>>> GetUserVideosAsync();
|
||||||
|
Task<Result<List<VideoResponse>>> GetVideosByChannelIdAsync(string channelId);
|
||||||
|
Task<Result<VideoResponse>> GetVideoDetailsAsync(string videoId);
|
||||||
|
Task<Result<List<VideoResponse>>> GetChannelVideosFromYouTubeAsync(string channelId, int maxResults = 10);
|
||||||
|
Task<Result<bool>> AddVideoAsync(string videoId, string channelId);
|
||||||
|
Task<Result<bool>> AddVideosAsync(List<string> videoIds, string channelId);
|
||||||
|
Task<Result<bool>> RemoveVideoAsync(string videoId);
|
||||||
|
Task<Result<List<VideoResponse>>> SearchVideosAsync(string query, int maxResults = 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
308
Postall.Domain/Services/VideoService .cs
Normal file
308
Postall.Domain/Services/VideoService .cs
Normal file
@ -0,0 +1,308 @@
|
|||||||
|
using BaseDomain.Results;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Postall.Domain;
|
||||||
|
using Postall.Domain.Contracts.Repositories;
|
||||||
|
using Postall.Domain.Dtos;
|
||||||
|
using Postall.Domain.Entities;
|
||||||
|
using Postall.Domain.Services.Contracts;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Postall.Infra.Services
|
||||||
|
{
|
||||||
|
public class VideoService : IVideoService
|
||||||
|
{
|
||||||
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
|
private readonly IVideoRepository _videoRepository;
|
||||||
|
private readonly IChannelRepository _channelRepository;
|
||||||
|
private readonly IVideoYoutubeService _videoYoutubeService;
|
||||||
|
|
||||||
|
public VideoService(
|
||||||
|
IHttpContextAccessor httpContextAccessor,
|
||||||
|
IVideoRepository videoRepository,
|
||||||
|
IChannelRepository channelRepository,
|
||||||
|
IVideoYoutubeService videoYoutubeService)
|
||||||
|
{
|
||||||
|
_httpContextAccessor = httpContextAccessor;
|
||||||
|
_videoRepository = videoRepository;
|
||||||
|
_channelRepository = channelRepository;
|
||||||
|
_videoYoutubeService = videoYoutubeService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtém o ID do usuário atual a partir das claims
|
||||||
|
/// </summary>
|
||||||
|
private string GetCurrentUserId()
|
||||||
|
{
|
||||||
|
var user = _httpContextAccessor.HttpContext.User;
|
||||||
|
var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
{
|
||||||
|
var email = user.FindFirst(ClaimTypes.Email)?.Value;
|
||||||
|
if (!string.IsNullOrEmpty(email))
|
||||||
|
{
|
||||||
|
userId = Convert.ToBase64String(
|
||||||
|
System.Text.Encoding.UTF8.GetBytes(email)
|
||||||
|
).Replace("/", "_").Replace("+", "-").Replace("=", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtém todos os vídeos do usuário atual
|
||||||
|
/// </summary>
|
||||||
|
public async Task<Result<List<VideoResponse>>> GetUserVideosAsync()
|
||||||
|
{
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
return new List<VideoResponse>();
|
||||||
|
|
||||||
|
var videos = await _videoRepository.GetByUserIdAsync(userId);
|
||||||
|
return videos.Select(v => v.ToVideoResponse()).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtém os vídeos de um canal específico para o usuário atual
|
||||||
|
/// </summary>
|
||||||
|
public async Task<Result<List<VideoResponse>>> GetVideosByChannelIdAsync(string channelId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(channelId))
|
||||||
|
return new List<VideoResponse>();
|
||||||
|
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
return new List<VideoResponse>();
|
||||||
|
|
||||||
|
var videos = await _videoRepository.GetByUserIdAndChannelIdAsync(userId, channelId);
|
||||||
|
return videos.Select(v => v.ToVideoResponse()).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtém os detalhes de um vídeo específico
|
||||||
|
/// </summary>
|
||||||
|
public async Task<Result<VideoResponse>> GetVideoDetailsAsync(string videoId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(videoId))
|
||||||
|
return Result<VideoResponse>.Failure("ID do vídeo inválido");
|
||||||
|
|
||||||
|
var videoDetails = await _videoYoutubeService.GetVideoDetailsAsync(videoId);
|
||||||
|
return videoDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtém os vídeos de um canal diretamente do YouTube
|
||||||
|
/// </summary>
|
||||||
|
public async Task<Result<List<VideoResponse>>> GetChannelVideosFromYouTubeAsync(string channelId, int maxResults = 10)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(channelId))
|
||||||
|
return Result<List<VideoResponse>>.Failure("ID do canal inválido");
|
||||||
|
|
||||||
|
// Verifica se o canal existe para o usuário
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
var channel = await _channelRepository.GetByUserIdAndChannelIdAsync(userId, channelId);
|
||||||
|
|
||||||
|
if (channel == null)
|
||||||
|
return Result<List<VideoResponse>>.Failure("Canal não encontrado para este usuário");
|
||||||
|
|
||||||
|
// Obtém os vídeos que o usuário já adicionou para este canal
|
||||||
|
var userVideos = await _videoRepository.GetByUserIdAndChannelIdAsync(userId, channelId);
|
||||||
|
var userVideoIds = userVideos.Select(v => v.VideoId).ToHashSet();
|
||||||
|
|
||||||
|
// Busca os vídeos do canal no YouTube
|
||||||
|
var channelVideos = await _videoYoutubeService.GetChannelVideosAsync(channelId, maxResults);
|
||||||
|
|
||||||
|
if (!channelVideos.IsSuccess)
|
||||||
|
return channelVideos;
|
||||||
|
|
||||||
|
// Marca os vídeos que já foram adicionados pelo usuário
|
||||||
|
foreach (var video in channelVideos.Value)
|
||||||
|
{
|
||||||
|
if (userVideoIds.Contains(video.VideoId))
|
||||||
|
{
|
||||||
|
// Esse vídeo já foi adicionado pelo usuário
|
||||||
|
video.Id = userVideos.First(v => v.VideoId == video.VideoId).Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return channelVideos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adiciona um vídeo à lista do usuário
|
||||||
|
/// </summary>
|
||||||
|
public async Task<Result<bool>> AddVideoAsync(string videoId, string channelId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(videoId))
|
||||||
|
return Result<bool>.Failure("ID do vídeo inválido");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(channelId))
|
||||||
|
return Result<bool>.Failure("ID do canal inválido");
|
||||||
|
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
return Result<bool>.Failure("Usuário não identificado");
|
||||||
|
|
||||||
|
// Verifica se o canal existe para o usuário
|
||||||
|
var channel = await _channelRepository.GetByUserIdAndChannelIdAsync(userId, channelId);
|
||||||
|
|
||||||
|
if (channel == null)
|
||||||
|
return Result<bool>.Failure("Canal não encontrado para este usuário");
|
||||||
|
|
||||||
|
// Verifica se o vídeo já existe para o usuário
|
||||||
|
var existingVideo = await _videoRepository.GetByUserIdAndVideoIdAsync(userId, videoId);
|
||||||
|
|
||||||
|
if (existingVideo != null)
|
||||||
|
return true; // Vídeo já existe, retorna sucesso
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Obtém os detalhes do vídeo do YouTube
|
||||||
|
var videoDetails = await _videoYoutubeService.GetVideoDetailsAsync(videoId);
|
||||||
|
|
||||||
|
if (!videoDetails.IsSuccess)
|
||||||
|
return Result<bool>.Failure(videoDetails.Error);
|
||||||
|
|
||||||
|
// Cria o objeto de dados do vídeo
|
||||||
|
var videoData = new VideoData
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
ChannelId = channelId,
|
||||||
|
VideoId = videoId,
|
||||||
|
Title = videoDetails.Value.Title,
|
||||||
|
Description = videoDetails.Value.Description,
|
||||||
|
ThumbnailUrl = videoDetails.Value.ThumbnailUrl,
|
||||||
|
PublishedAt = videoDetails.Value.PublishedAt,
|
||||||
|
ViewCount = videoDetails.Value.ViewCount,
|
||||||
|
LikeCount = videoDetails.Value.LikeCount,
|
||||||
|
DislikeCount = videoDetails.Value.DislikeCount,
|
||||||
|
CommentCount = videoDetails.Value.CommentCount,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
// Adiciona o vídeo ao repositório
|
||||||
|
await _videoRepository.AddAsync(videoData);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<bool>.Failure(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adiciona múltiplos vídeos à lista do usuário
|
||||||
|
/// </summary>
|
||||||
|
public async Task<Result<bool>> AddVideosAsync(List<string> videoIds, string channelId)
|
||||||
|
{
|
||||||
|
if (videoIds == null || !videoIds.Any())
|
||||||
|
return Result<bool>.Failure("Lista de IDs de vídeos inválida");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(channelId))
|
||||||
|
return Result<bool>.Failure("ID do canal inválido");
|
||||||
|
|
||||||
|
var userId = GetCurrentUserId();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(userId))
|
||||||
|
return Result<bool>.Failure("Usuário não identificado");
|
||||||
|
|
||||||
|
// Verifica se o canal existe para o usuário
|
||||||
|
var channel = await _channelRepository.GetByUserIdAndChannelIdAsync(userId, channelId);
|
||||||
|
|
||||||
|
if (channel == null)
|
||||||
|
return Result<bool>.Failure("Canal não encontrado para este usuário");
|
||||||
|
|
||||||
|
// Obtém os vídeos que o usuário já adicionou
|
||||||
|
var existingVideos = await _videoRepository.GetByUserIdAsync(userId);
|
||||||
|
var existingVideoIds = existingVideos.Select(v => v.VideoId).ToHashSet();
|
||||||
|
|
||||||
|
// Filtra os vídeos que ainda não foram adicionados
|
||||||
|
var newVideoIds = videoIds.Where(id => !existingVideoIds.Contains(id)).ToList();
|
||||||
|
|
||||||
|
if (newVideoIds.Count == 0)
|
||||||
|
return true; // Todos os vídeos já existem
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var videoDataList = new List<VideoData>();
|
||||||
|
|
||||||
|
// Obtém os detalhes de cada vídeo do YouTube e cria os objetos de dados
|
||||||
|
foreach (var videoId in newVideoIds)
|
||||||
|
{
|
||||||
|
var videoDetails = await _videoYoutubeService.GetVideoDetailsAsync(videoId);
|
||||||
|
|
||||||
|
if (videoDetails.IsSuccess)
|
||||||
|
{
|
||||||
|
videoDataList.Add(new VideoData
|
||||||
|
{
|
||||||
|
UserId = userId,
|
||||||
|
ChannelId = channelId,
|
||||||
|
VideoId = videoId,
|
||||||
|
Title = videoDetails.Value.Title,
|
||||||
|
Description = videoDetails.Value.Description,
|
||||||
|
ThumbnailUrl = videoDetails.Value.ThumbnailUrl,
|
||||||
|
PublishedAt = videoDetails.Value.PublishedAt,
|
||||||
|
ViewCount = videoDetails.Value.ViewCount,
|
||||||
|
LikeCount = videoDetails.Value.LikeCount,
|
||||||
|
DislikeCount = videoDetails.Value.DislikeCount,
|
||||||
|
CommentCount = videoDetails.Value.CommentCount,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adiciona os vídeos ao repositório
|
||||||
|
if (videoDataList.Any())
|
||||||
|
{
|
||||||
|
await _videoRepository.AddManyAsync(videoDataList);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<bool>.Failure(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove um vídeo da lista do usuário
|
||||||
|
/// </summary>
|
||||||
|
public async Task<Result<bool>> RemoveVideoAsync(string videoId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(videoId))
|
||||||
|
return Result<bool>.Failure("ID do vídeo inválido");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _videoRepository.DeleteAsync(videoId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<bool>.Failure(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Busca vídeos no YouTube pelo título ou descrição
|
||||||
|
/// </summary>
|
||||||
|
public async Task<Result<List<VideoResponse>>> SearchVideosAsync(string query, int maxResults = 10)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(query) || query.Length < 3)
|
||||||
|
return new List<VideoResponse>();
|
||||||
|
|
||||||
|
var results = await _videoYoutubeService.SearchVideosAsync(query, maxResults);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Postall.Domain;
|
using Postall.Domain;
|
||||||
|
using Postall.Domain.Contracts.Repositories;
|
||||||
using Postall.Domain.Services.Contracts;
|
using Postall.Domain.Services.Contracts;
|
||||||
using Postall.Infra.MongoDB.Repositories;
|
using Postall.Infra.MongoDB.Repositories;
|
||||||
using Postall.Infra.Services;
|
using Postall.Infra.Services;
|
||||||
@ -16,6 +17,8 @@ namespace Postall.Infra.MongoDB.Extensions
|
|||||||
public static IServiceCollection AddRepositories(this IServiceCollection services)
|
public static IServiceCollection AddRepositories(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddScoped<IChannelRepository, ChannelRepository>();
|
services.AddScoped<IChannelRepository, ChannelRepository>();
|
||||||
|
services.AddScoped<IVideoRepository, VideoRepository>();
|
||||||
|
services.AddScoped<IUserSocialRepository, UserSocialRepository>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using MongoDB.Bson;
|
using MongoDB.Bson;
|
||||||
using MongoDB.Driver;
|
using MongoDB.Driver;
|
||||||
using Postall.Domain;
|
using Postall.Domain.Contracts.Repositories;
|
||||||
using Postall.Domain.Dtos;
|
using Postall.Domain.Dtos;
|
||||||
using Postall.Domain.Entities;
|
using Postall.Domain.Entities;
|
||||||
using Postall.Infra.MongoDB.Settings;
|
using Postall.Infra.MongoDB.Settings;
|
||||||
@ -25,7 +25,7 @@ namespace Postall.Infra.MongoDB.Repositories
|
|||||||
var database = client.GetDatabase(mongoDbSettings.Value.DatabaseName);
|
var database = client.GetDatabase(mongoDbSettings.Value.DatabaseName);
|
||||||
_channelsCollection = database.GetCollection<ChannelData>(mongoDbSettings.Value.ChannelsCollectionName);
|
_channelsCollection = database.GetCollection<ChannelData>(mongoDbSettings.Value.ChannelsCollectionName);
|
||||||
|
|
||||||
var indexKeysDefinition = Builders<ChannelData>.IndexKeys.Ascending(c => c.YoutubeId);
|
var indexKeysDefinition = Builders<ChannelData>.IndexKeys.Ascending(c => c.ChannelId).Ascending(c => c.UserId);
|
||||||
_channelsCollection.Indexes.CreateOne(new CreateIndexModel<ChannelData>(indexKeysDefinition, new CreateIndexOptions { Unique = true }));
|
_channelsCollection.Indexes.CreateOne(new CreateIndexModel<ChannelData>(indexKeysDefinition, new CreateIndexOptions { Unique = true }));
|
||||||
|
|
||||||
var textIndexDefinition = Builders<ChannelData>.IndexKeys
|
var textIndexDefinition = Builders<ChannelData>.IndexKeys
|
||||||
@ -79,18 +79,30 @@ namespace Postall.Infra.MongoDB.Repositories
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<ChannelData> AddAsync(ChannelData ChannelData)
|
public async Task<ChannelData> AddAsync(ChannelData ChannelData)
|
||||||
{
|
{
|
||||||
// Verifica se o canal já existe pelo ID do YouTube
|
var existingChannel = await GetByUserIdAndChannelIdAsync(ChannelData.UserId, ChannelData.ChannelId);
|
||||||
var existingChannel = await GetByYoutubeIdAsync(ChannelData.YoutubeId);
|
|
||||||
if (existingChannel != null)
|
if (existingChannel != null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
// Gera novo ID para o MongoDB se não for fornecido
|
|
||||||
if (string.IsNullOrEmpty(ChannelData.Id) || !ObjectId.TryParse(ChannelData.Id, out _))
|
if (string.IsNullOrEmpty(ChannelData.Id) || !ObjectId.TryParse(ChannelData.Id, out _))
|
||||||
{
|
{
|
||||||
ChannelData.Id = ObjectId.GenerateNewId().ToString();
|
ChannelData.Id = ObjectId.GenerateNewId().ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
await _channelsCollection.InsertOneAsync(ChannelData);
|
try
|
||||||
|
{
|
||||||
|
await _channelsCollection.InsertOneAsync(ChannelData);
|
||||||
|
}
|
||||||
|
catch (MongoWriteException ex)
|
||||||
|
{
|
||||||
|
if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
|
||||||
|
{
|
||||||
|
return await GetByYoutubeIdAsync(ChannelData.YoutubeId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
return ChannelData;
|
return ChannelData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
210
Postall.Infra.MongoDB/Repositories/UserSocialRepository.cs
Normal file
210
Postall.Infra.MongoDB/Repositories/UserSocialRepository.cs
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using MongoDB.Bson;
|
||||||
|
using MongoDB.Driver;
|
||||||
|
using Postall.Domain;
|
||||||
|
using Postall.Domain.Entities;
|
||||||
|
using Postall.Infra.MongoDB.Settings;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Postall.Infra.MongoDB.Repositories
|
||||||
|
{
|
||||||
|
public class UserSocialRepository : IUserSocialRepository
|
||||||
|
{
|
||||||
|
private readonly IMongoCollection<UserSocialData> _userSocialCollection;
|
||||||
|
|
||||||
|
public UserSocialRepository(IOptions<MongoDbSettings> mongoDbSettings)
|
||||||
|
{
|
||||||
|
var client = new MongoClient(mongoDbSettings.Value.ConnectionString);
|
||||||
|
var database = client.GetDatabase(mongoDbSettings.Value.DatabaseName);
|
||||||
|
_userSocialCollection = database.GetCollection<UserSocialData>(mongoDbSettings.Value.UserSocialCollectionName);
|
||||||
|
|
||||||
|
// Cria índice para userId (deve ser único)
|
||||||
|
var indexKeysDefinition = Builders<UserSocialData>.IndexKeys
|
||||||
|
.Ascending(u => u.UserId);
|
||||||
|
_userSocialCollection.Indexes.CreateOne(new CreateIndexModel<UserSocialData>(indexKeysDefinition, new CreateIndexOptions { Unique = true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtém todos os dados sociais dos usuários
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IEnumerable<UserSocialData>> GetAllAsync()
|
||||||
|
{
|
||||||
|
return await _userSocialCollection.Find(u => true).ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtém os dados sociais pelo ID do MongoDB
|
||||||
|
/// </summary>
|
||||||
|
public async Task<UserSocialData> GetByIdAsync(string id)
|
||||||
|
{
|
||||||
|
if (!ObjectId.TryParse(id, out _))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return await _userSocialCollection.Find(u => u.Id == ObjectId.Parse(id)).FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtém os dados sociais pelo ID do usuário
|
||||||
|
/// </summary>
|
||||||
|
public async Task<UserSocialData> GetByUserIdAsync(string userId)
|
||||||
|
{
|
||||||
|
return await _userSocialCollection.Find(u => u.UserId == userId).FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtém os dados sociais pelo token do Google
|
||||||
|
/// </summary>
|
||||||
|
public async Task<UserSocialData> GetByGoogleTokenAsync(string googleToken)
|
||||||
|
{
|
||||||
|
return await _userSocialCollection.Find(u => u.GoogleToken == googleToken).FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adiciona novos dados sociais de usuário
|
||||||
|
/// </summary>
|
||||||
|
public async Task<UserSocialData> AddAsync(UserSocialData userSocialData)
|
||||||
|
{
|
||||||
|
var existingUserSocial = await GetByUserIdAsync(userSocialData.UserId);
|
||||||
|
if (existingUserSocial != null)
|
||||||
|
return existingUserSocial;
|
||||||
|
|
||||||
|
if (userSocialData.Id == ObjectId.Empty)
|
||||||
|
{
|
||||||
|
userSocialData.Id = ObjectId.GenerateNewId();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _userSocialCollection.InsertOneAsync(userSocialData);
|
||||||
|
}
|
||||||
|
catch (MongoWriteException ex)
|
||||||
|
{
|
||||||
|
if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
|
||||||
|
{
|
||||||
|
return await GetByUserIdAsync(userSocialData.UserId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return userSocialData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Atualiza os dados sociais de um usuário existente
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> UpdateAsync(UserSocialData userSocialData)
|
||||||
|
{
|
||||||
|
if (userSocialData.Id == ObjectId.Empty)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var result = await _userSocialCollection.ReplaceOneAsync(
|
||||||
|
u => u.Id == userSocialData.Id,
|
||||||
|
userSocialData,
|
||||||
|
new ReplaceOptions { IsUpsert = false });
|
||||||
|
|
||||||
|
return result.IsAcknowledged && result.ModifiedCount > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Atualiza os dados sociais de um usuário existente
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> UpdateOneAsync(UserSocialData userSocialData)
|
||||||
|
{
|
||||||
|
if (userSocialData.Id == ObjectId.Empty)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var result = await _userSocialCollection.ReplaceOneAsync(
|
||||||
|
u => u.Id == userSocialData.Id,
|
||||||
|
userSocialData,
|
||||||
|
new ReplaceOptions { IsUpsert = false });
|
||||||
|
|
||||||
|
return result.IsAcknowledged && result.ModifiedCount > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Atualiza ou insere os dados sociais de um usuário
|
||||||
|
/// </summary>
|
||||||
|
public async Task<UserSocialData> UpsertAsync(UserSocialData userSocialData)
|
||||||
|
{
|
||||||
|
var existingData = await GetByUserIdAsync(userSocialData.UserId);
|
||||||
|
|
||||||
|
if (existingData != null)
|
||||||
|
{
|
||||||
|
userSocialData.Id = existingData.Id;
|
||||||
|
await _userSocialCollection.ReplaceOneAsync(
|
||||||
|
u => u.Id == existingData.Id,
|
||||||
|
userSocialData,
|
||||||
|
new ReplaceOptions { IsUpsert = true });
|
||||||
|
return userSocialData;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return await AddAsync(userSocialData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove os dados sociais pelo ID do MongoDB
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> DeleteAsync(string id)
|
||||||
|
{
|
||||||
|
if (!ObjectId.TryParse(id, out _))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var result = await _userSocialCollection.DeleteOneAsync(u => u.Id == ObjectId.Parse(id));
|
||||||
|
return result.IsAcknowledged && result.DeletedCount > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove os dados sociais pelo ID do usuário
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> DeleteByUserIdAsync(string userId)
|
||||||
|
{
|
||||||
|
var result = await _userSocialCollection.DeleteOneAsync(u => u.UserId == userId);
|
||||||
|
return result.IsAcknowledged && result.DeletedCount > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Atualiza apenas o token do Google para um usuário
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> UpdateGoogleTokenAsync(string userId, string googleToken)
|
||||||
|
{
|
||||||
|
var update = Builders<UserSocialData>.Update.Set(u => u.GoogleToken, googleToken);
|
||||||
|
var result = await _userSocialCollection.UpdateOneAsync(
|
||||||
|
u => u.UserId == userId,
|
||||||
|
update,
|
||||||
|
new UpdateOptions { IsUpsert = true });
|
||||||
|
|
||||||
|
return result.IsAcknowledged && (result.ModifiedCount > 0 || result.UpsertedId != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Atualiza apenas o token do Facebook para um usuário
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> UpdateFacebookTokenAsync(string userId, FacebookToken facebookToken)
|
||||||
|
{
|
||||||
|
var update = Builders<UserSocialData>.Update.Set(u => u.FacebookToken, facebookToken);
|
||||||
|
var result = await _userSocialCollection.UpdateOneAsync(
|
||||||
|
u => u.UserId == userId,
|
||||||
|
update,
|
||||||
|
new UpdateOptions { IsUpsert = true });
|
||||||
|
|
||||||
|
return result.IsAcknowledged && (result.ModifiedCount > 0 || result.UpsertedId != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UpdateOneAsync(string userId, UpdateDefinition<UserSocialData> update)
|
||||||
|
{
|
||||||
|
var result = await _userSocialCollection.UpdateOneAsync(
|
||||||
|
x => x.UserId == userId,
|
||||||
|
update,
|
||||||
|
new UpdateOptions { IsUpsert = true });
|
||||||
|
|
||||||
|
return result.IsAcknowledged && (result.ModifiedCount > 0 || result.UpsertedId != null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
253
Postall.Infra.MongoDB/Repositories/VideoRepository.cs
Normal file
253
Postall.Infra.MongoDB/Repositories/VideoRepository.cs
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using MongoDB.Bson;
|
||||||
|
using MongoDB.Driver;
|
||||||
|
using Postall.Domain;
|
||||||
|
using Postall.Domain.Entities;
|
||||||
|
using Postall.Infra.MongoDB.Settings;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Postall.Infra.MongoDB.Repositories
|
||||||
|
{
|
||||||
|
public class VideoRepository : IVideoRepository
|
||||||
|
{
|
||||||
|
private readonly IMongoCollection<VideoData> _videosCollection;
|
||||||
|
|
||||||
|
public VideoRepository(IOptions<MongoDbSettings> mongoDbSettings)
|
||||||
|
{
|
||||||
|
var client = new MongoClient(mongoDbSettings.Value.ConnectionString);
|
||||||
|
var database = client.GetDatabase(mongoDbSettings.Value.DatabaseName);
|
||||||
|
_videosCollection = database.GetCollection<VideoData>(mongoDbSettings.Value.VideosCollectionName);
|
||||||
|
|
||||||
|
// Cria índice composto para videoId e userId
|
||||||
|
var indexKeysDefinition = Builders<VideoData>.IndexKeys
|
||||||
|
.Ascending(v => v.VideoId)
|
||||||
|
.Ascending(v => v.UserId);
|
||||||
|
_videosCollection.Indexes.CreateOne(new CreateIndexModel<VideoData>(indexKeysDefinition, new CreateIndexOptions { Unique = true }));
|
||||||
|
|
||||||
|
// Cria índice para buscas por texto
|
||||||
|
var textIndexDefinition = Builders<VideoData>.IndexKeys
|
||||||
|
.Text(v => v.Title)
|
||||||
|
.Text(v => v.Description);
|
||||||
|
_videosCollection.Indexes.CreateOne(new CreateIndexModel<VideoData>(textIndexDefinition));
|
||||||
|
|
||||||
|
// Cria índice para buscas por channelId
|
||||||
|
var channelIndexDefinition = Builders<VideoData>.IndexKeys
|
||||||
|
.Ascending(v => v.ChannelId);
|
||||||
|
_videosCollection.Indexes.CreateOne(new CreateIndexModel<VideoData>(channelIndexDefinition));
|
||||||
|
|
||||||
|
// Cria índice para userId + channelId
|
||||||
|
var userChannelIndexDefinition = Builders<VideoData>.IndexKeys
|
||||||
|
.Ascending(v => v.UserId)
|
||||||
|
.Ascending(v => v.ChannelId);
|
||||||
|
_videosCollection.Indexes.CreateOne(new CreateIndexModel<VideoData>(userChannelIndexDefinition));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtém todos os vídeos
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IEnumerable<VideoData>> GetAllAsync()
|
||||||
|
{
|
||||||
|
return await _videosCollection.Find(v => true).ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtém um vídeo pelo ID do MongoDB
|
||||||
|
/// </summary>
|
||||||
|
public async Task<VideoData> GetByIdAsync(string id)
|
||||||
|
{
|
||||||
|
if (!ObjectId.TryParse(id, out _))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return await _videosCollection.Find(v => v.Id == id).FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtém um vídeo pelo ID do YouTube
|
||||||
|
/// </summary>
|
||||||
|
public async Task<VideoData> GetByVideoIdAsync(string videoId)
|
||||||
|
{
|
||||||
|
return await _videosCollection.Find(v => v.VideoId == videoId).FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtém os vídeos pelo ID do usuário
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IList<VideoData>> GetByUserIdAsync(string userId)
|
||||||
|
{
|
||||||
|
return await _videosCollection.Find(v => v.UserId == userId).ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtém os vídeos pelo ID do canal
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IList<VideoData>> GetByChannelIdAsync(string channelId)
|
||||||
|
{
|
||||||
|
return await _videosCollection.Find(v => v.ChannelId == channelId).ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtém os vídeos pelo ID do usuário e ID do canal
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IList<VideoData>> GetByUserIdAndChannelIdAsync(string userId, string channelId)
|
||||||
|
{
|
||||||
|
return await _videosCollection.Find(v => v.UserId == userId && v.ChannelId == channelId).ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtém um vídeo pelo ID do usuário e ID do vídeo
|
||||||
|
/// </summary>
|
||||||
|
public async Task<VideoData> GetByUserIdAndVideoIdAsync(string userId, string videoId)
|
||||||
|
{
|
||||||
|
return await _videosCollection.Find(v => v.UserId == userId && v.VideoId == videoId).FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adiciona um novo vídeo
|
||||||
|
/// </summary>
|
||||||
|
public async Task<VideoData> AddAsync(VideoData videoData)
|
||||||
|
{
|
||||||
|
var existingVideo = await GetByUserIdAndVideoIdAsync(videoData.UserId, videoData.VideoId);
|
||||||
|
if (existingVideo != null)
|
||||||
|
return existingVideo;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(videoData.Id) || !ObjectId.TryParse(videoData.Id, out _))
|
||||||
|
{
|
||||||
|
videoData.Id = ObjectId.GenerateNewId().ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
videoData.CreatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _videosCollection.InsertOneAsync(videoData);
|
||||||
|
}
|
||||||
|
catch (MongoWriteException ex)
|
||||||
|
{
|
||||||
|
if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
|
||||||
|
{
|
||||||
|
return await GetByUserIdAndVideoIdAsync(videoData.UserId, videoData.VideoId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return videoData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adiciona vários vídeos de uma vez
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IEnumerable<VideoData>> AddManyAsync(IEnumerable<VideoData> videos)
|
||||||
|
{
|
||||||
|
if (videos == null || !videos.Any())
|
||||||
|
return new List<VideoData>();
|
||||||
|
|
||||||
|
var videosList = videos.ToList();
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// Gera novos IDs para o MongoDB se necessário
|
||||||
|
foreach (var videoData in videosList)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(videoData.Id) || !ObjectId.TryParse(videoData.Id, out _))
|
||||||
|
{
|
||||||
|
videoData.Id = ObjectId.GenerateNewId().ToString();
|
||||||
|
}
|
||||||
|
videoData.CreatedAt = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifica vídeos existentes pelo ID do YouTube e usuário
|
||||||
|
var userIds = videosList.Select(v => v.UserId).Distinct().ToList();
|
||||||
|
var videoIds = videosList.Select(v => v.VideoId).ToList();
|
||||||
|
|
||||||
|
var filter = Builders<VideoData>.Filter.And(
|
||||||
|
Builders<VideoData>.Filter.In(v => v.UserId, userIds),
|
||||||
|
Builders<VideoData>.Filter.In(v => v.VideoId, videoIds)
|
||||||
|
);
|
||||||
|
|
||||||
|
var existingVideos = await _videosCollection.Find(filter).ToListAsync();
|
||||||
|
|
||||||
|
// Filtra apenas os vídeos que ainda não foram adicionados
|
||||||
|
var existingPairs = existingVideos
|
||||||
|
.Select(v => $"{v.UserId}:{v.VideoId}")
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
var newVideos = videosList
|
||||||
|
.Where(v => !existingPairs.Contains($"{v.UserId}:{v.VideoId}"))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (newVideos.Any())
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _videosCollection.InsertManyAsync(newVideos);
|
||||||
|
}
|
||||||
|
catch (MongoBulkWriteException)
|
||||||
|
{
|
||||||
|
// Em caso de erro, adiciona um por um
|
||||||
|
foreach (var video in newVideos)
|
||||||
|
{
|
||||||
|
try { await AddAsync(video); }
|
||||||
|
catch { /* Ignora erros individuais */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newVideos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Atualiza um vídeo existente
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> UpdateAsync(VideoData videoData)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(videoData.Id) || !ObjectId.TryParse(videoData.Id, out _))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
videoData.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
var result = await _videosCollection.ReplaceOneAsync(
|
||||||
|
v => v.Id == videoData.Id,
|
||||||
|
videoData,
|
||||||
|
new ReplaceOptions { IsUpsert = false });
|
||||||
|
|
||||||
|
return result.IsAcknowledged && result.ModifiedCount > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove um vídeo pelo ID do MongoDB
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> DeleteAsync(string id)
|
||||||
|
{
|
||||||
|
if (!ObjectId.TryParse(id, out _))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var result = await _videosCollection.DeleteOneAsync(v => v.Id == id);
|
||||||
|
return result.IsAcknowledged && result.DeletedCount > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove um vídeo pelo ID do YouTube
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> DeleteByVideoIdAsync(string videoId)
|
||||||
|
{
|
||||||
|
var result = await _videosCollection.DeleteOneAsync(v => v.VideoId == videoId);
|
||||||
|
return result.IsAcknowledged && result.DeletedCount > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Busca vídeos com base em um termo de pesquisa no título ou descrição
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IEnumerable<VideoData>> SearchAsync(string searchTerm)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(searchTerm))
|
||||||
|
return await GetAllAsync();
|
||||||
|
|
||||||
|
var filter = Builders<VideoData>.Filter.Text(searchTerm);
|
||||||
|
return await _videosCollection.Find(filter).ToListAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,5 +14,7 @@ namespace Postall.Infra.MongoDB.Settings
|
|||||||
public string ConnectionString { get; set; }
|
public string ConnectionString { get; set; }
|
||||||
public string DatabaseName { get; set; }
|
public string DatabaseName { get; set; }
|
||||||
public string ChannelsCollectionName { get; set; }
|
public string ChannelsCollectionName { get; set; }
|
||||||
|
public string VideosCollectionName { get; set; }
|
||||||
|
public string UserSocialCollectionName { get; internal set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,12 +11,16 @@ namespace BaseDomain.Extensions
|
|||||||
{
|
{
|
||||||
services.AddHttpContextAccessor();
|
services.AddHttpContextAccessor();
|
||||||
|
|
||||||
services.AddScoped<IVideoService, YouTubeServiceVideo>();
|
services.AddScoped<IYouTubeServiceChannel, YouTubeServiceChannel>();
|
||||||
|
|
||||||
services.AddScoped<IChannelService, ChannelVideoService>();
|
services.AddScoped<IChannelService, ChannelVideoService>();
|
||||||
|
|
||||||
services.AddScoped<IChannelYoutubeService, ChannelYoutubeService>();
|
services.AddScoped<IChannelYoutubeService, ChannelYoutubeService>();
|
||||||
|
|
||||||
|
services.AddScoped<IVideoYoutubeService, VideoYoutubeService>();
|
||||||
|
|
||||||
|
services.AddScoped<IVideoService, VideoService>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using MongoDB.Driver;
|
using MongoDB.Driver;
|
||||||
|
using Postall.Domain;
|
||||||
using Postall.Domain.Dtos.Responses;
|
using Postall.Domain.Dtos.Responses;
|
||||||
using Postall.Domain.Entities;
|
using Postall.Domain.Entities;
|
||||||
using Postall.Domain.Services.Contracts;
|
using Postall.Domain.Services.Contracts;
|
||||||
@ -14,14 +15,21 @@ namespace Postall.Infra.Services
|
|||||||
{
|
{
|
||||||
public class FacebookTokenService: IFacebookServices
|
public class FacebookTokenService: IFacebookServices
|
||||||
{
|
{
|
||||||
private readonly IMongoCollection<UserSocialData> _tokens;
|
|
||||||
private readonly IConfiguration _config;
|
private readonly IConfiguration _config;
|
||||||
|
private readonly IUserSocialRepository _userSocialRepository;
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
|
|
||||||
|
public FacebookTokenService(IConfiguration configuration, IHttpClientFactory httpClientFactory, IUserSocialRepository userSocialRepository)
|
||||||
|
{
|
||||||
|
_config = configuration;
|
||||||
|
this._userSocialRepository = userSocialRepository;
|
||||||
|
_httpClient = httpClientFactory.CreateClient("Facebook");
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<string> GetLongLivedToken(string shortLivedToken)
|
public async Task<string> GetLongLivedToken(string shortLivedToken)
|
||||||
{
|
{
|
||||||
var appId = _config["Authentication:Facebook:AppId"];
|
var appId = _config.GetSection("Authentication:Facebook:AppId").Value;
|
||||||
var appSecret = _config["Authentication:Facebook:AppSecret"];
|
var appSecret = _config.GetSection("Authentication:Facebook:AppSecret").Value;
|
||||||
|
|
||||||
var response = await _httpClient.GetFromJsonAsync<FacebookTokenResponse>(
|
var response = await _httpClient.GetFromJsonAsync<FacebookTokenResponse>(
|
||||||
$"https://graph.facebook.com/oauth/access_token?" +
|
$"https://graph.facebook.com/oauth/access_token?" +
|
||||||
@ -39,11 +47,21 @@ namespace Postall.Infra.Services
|
|||||||
.Set(x => x.FacebookToken.AccessToken, token)
|
.Set(x => x.FacebookToken.AccessToken, token)
|
||||||
.Set(x => x.FacebookToken.ExpiresAt, DateTime.UtcNow.AddDays(60));
|
.Set(x => x.FacebookToken.ExpiresAt, DateTime.UtcNow.AddDays(60));
|
||||||
|
|
||||||
await _tokens.UpdateOneAsync(
|
if (_userSocialRepository.GetByIdAsync(userId) == null)
|
||||||
x => x.UserId == userId,
|
{
|
||||||
update,
|
await _userSocialRepository.AddAsync(new UserSocialData() { UserId = Guid.NewGuid().ToString("N")});
|
||||||
new UpdateOptions { IsUpsert = true }
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _userSocialRepository.UpdateOneAsync(
|
||||||
|
userId,
|
||||||
|
update
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task UpdateOneAsync(Func<object, bool> value, UpdateDefinition<UserSocialData> update, UpdateOptions updateOptions)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
134
Postall.Infra/Services/VideoYoutubeService .cs
Normal file
134
Postall.Infra/Services/VideoYoutubeService .cs
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
using BaseDomain.Results;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Postall.Domain.Dtos;
|
||||||
|
using Postall.Domain.Services.Contracts;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Postall.Infra.Services
|
||||||
|
{
|
||||||
|
public class VideoYoutubeService : IVideoYoutubeService
|
||||||
|
{
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly string _apiKey;
|
||||||
|
|
||||||
|
public VideoYoutubeService(IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_configuration = configuration;
|
||||||
|
_apiKey = _configuration["YouTube:ApiKey"];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Busca os detalhes de um vídeo na API do YouTube
|
||||||
|
/// </summary>
|
||||||
|
public async Task<Result<VideoResponse>> GetVideoDetailsAsync(string videoId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var youtubeService = new YouTubeBaseServiceWrapper(_apiKey);
|
||||||
|
|
||||||
|
var videosRequest = youtubeService.Videos.List("snippet,statistics");
|
||||||
|
videosRequest.Id = videoId;
|
||||||
|
|
||||||
|
var response = await videosRequest.ExecuteAsync();
|
||||||
|
|
||||||
|
if (response.Items.Count == 0)
|
||||||
|
return Result<VideoResponse>.Failure($"Vídeo não encontrado: {videoId}");
|
||||||
|
|
||||||
|
var video = response.Items[0];
|
||||||
|
var snippet = video.Snippet;
|
||||||
|
var statistics = video.Statistics;
|
||||||
|
|
||||||
|
return new VideoResponse
|
||||||
|
{
|
||||||
|
VideoId = video.Id,
|
||||||
|
ChannelId = snippet.ChannelId,
|
||||||
|
Title = snippet.Title,
|
||||||
|
Description = snippet.Description,
|
||||||
|
ThumbnailUrl = snippet.Thumbnails.Medium?.Url ?? snippet.Thumbnails.Default__.Url,
|
||||||
|
PublishedAt = snippet.PublishedAt.GetValueOrDefault(),
|
||||||
|
ViewCount = statistics.ViewCount.GetValueOrDefault(),
|
||||||
|
LikeCount = statistics.LikeCount.GetValueOrDefault(),
|
||||||
|
DislikeCount = statistics.DislikeCount.GetValueOrDefault(),
|
||||||
|
CommentCount = statistics.CommentCount.GetValueOrDefault()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<VideoResponse>.Failure(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Busca vídeos de um canal na API do YouTube
|
||||||
|
/// </summary>
|
||||||
|
public async Task<Result<List<VideoResponse>>> GetChannelVideosAsync(string channelId, int maxResults = 50)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var youtubeService = new YouTubeBaseServiceWrapper(_apiKey);
|
||||||
|
|
||||||
|
var searchRequest = youtubeService.Search.List("snippet");
|
||||||
|
searchRequest.ChannelId = channelId;
|
||||||
|
searchRequest.Type = "video";
|
||||||
|
searchRequest.Order = Google.Apis.YouTube.v3.SearchResource.ListRequest.OrderEnum.Date;
|
||||||
|
searchRequest.MaxResults = maxResults;
|
||||||
|
|
||||||
|
var searchResponse = await searchRequest.ExecuteAsync();
|
||||||
|
var videos = new List<VideoResponse>();
|
||||||
|
|
||||||
|
foreach (var item in searchResponse.Items)
|
||||||
|
{
|
||||||
|
// Para cada resultado, obter os detalhes completos do vídeo
|
||||||
|
var videoResult = await GetVideoDetailsAsync(item.Id.VideoId);
|
||||||
|
if (videoResult.IsSuccess)
|
||||||
|
{
|
||||||
|
videos.Add(videoResult.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return videos;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<List<VideoResponse>>.Failure(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Busca vídeos pelo título ou descrição na API do YouTube
|
||||||
|
/// </summary>
|
||||||
|
public async Task<Result<List<VideoResponse>>> SearchVideosAsync(string query, int maxResults = 10)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var youtubeService = new YouTubeBaseServiceWrapper(_apiKey);
|
||||||
|
|
||||||
|
var searchRequest = youtubeService.Search.List("snippet");
|
||||||
|
searchRequest.Q = query;
|
||||||
|
searchRequest.Type = "video";
|
||||||
|
searchRequest.MaxResults = maxResults;
|
||||||
|
|
||||||
|
var searchResponse = await searchRequest.ExecuteAsync();
|
||||||
|
var videos = new List<VideoResponse>();
|
||||||
|
|
||||||
|
foreach (var item in searchResponse.Items)
|
||||||
|
{
|
||||||
|
// Para cada resultado, obter os detalhes completos do vídeo
|
||||||
|
var videoResult = await GetVideoDetailsAsync(item.Id.VideoId);
|
||||||
|
if (videoResult.IsSuccess)
|
||||||
|
{
|
||||||
|
videos.Add(videoResult.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return videos;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<List<VideoResponse>>.Failure(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,7 +8,7 @@ using System.Security.Claims;
|
|||||||
|
|
||||||
namespace Postall.Infra.Services
|
namespace Postall.Infra.Services
|
||||||
{
|
{
|
||||||
public class YouTubeServiceVideo: IVideoService
|
public class YouTubeServiceChannel: IYouTubeServiceChannel
|
||||||
{
|
{
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
@ -16,7 +16,7 @@ namespace Postall.Infra.Services
|
|||||||
private readonly string _clientId;
|
private readonly string _clientId;
|
||||||
private readonly string _clientSecret;
|
private readonly string _clientSecret;
|
||||||
|
|
||||||
public YouTubeServiceVideo(IConfiguration configuration, IHttpContextAccessor httpContextAccessor)
|
public YouTubeServiceChannel(IConfiguration configuration, IHttpContextAccessor httpContextAccessor)
|
||||||
{
|
{
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_httpContextAccessor = httpContextAccessor;
|
_httpContextAccessor = httpContextAccessor;
|
||||||
@ -18,7 +18,7 @@ namespace Postall.Controllers
|
|||||||
public async Task<IActionResult> Index()
|
public async Task<IActionResult> Index()
|
||||||
{
|
{
|
||||||
var userChannels = await _channelService.GetUserChannelsAsync();
|
var userChannels = await _channelService.GetUserChannelsAsync();
|
||||||
return View(userChannels);
|
return View(userChannels.IsSuccess ? userChannels.Value : new List<string>());
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
@ -87,7 +87,10 @@ namespace Postall.Controllers
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var results = await _channelService.SearchChannelsAsync(query);
|
var results = await _channelService.SearchChannelsAsync(query);
|
||||||
return Json(new { success = true, data = results });
|
if (results.IsSuccess)
|
||||||
|
return Json(new { success = true, data = results.Value });
|
||||||
|
|
||||||
|
return Json(new { success = false, message = "Erro ao buscar canais" });
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,75 +1,232 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Postall.Domain.Services.Contracts;
|
||||||
using Postall.Models;
|
using Postall.Models;
|
||||||
using System.Security.Claims;
|
using System;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Postall.Models;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Postall.Controllers
|
namespace Postall.Controllers
|
||||||
{
|
{
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public class VideosController : Controller
|
public class VideosController : Controller
|
||||||
{
|
{
|
||||||
// Simulação de dados para exemplo - em produção, estes dados viriam de uma API ou banco de dados
|
private readonly IVideoService _videoService;
|
||||||
private List<VideoViewModel> GetSampleVideos()
|
private readonly IChannelService _channelService;
|
||||||
|
|
||||||
|
public VideosController(IVideoService videoService, IChannelService channelService)
|
||||||
{
|
{
|
||||||
return new List<VideoViewModel>
|
_videoService = videoService;
|
||||||
|
_channelService = channelService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exibe a página principal com os vídeos organizados por canal
|
||||||
|
/// </summary>
|
||||||
|
public async Task<IActionResult> Index()
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
new VideoViewModel
|
// Obtém os canais do usuário
|
||||||
|
var userChannelsResult = await _channelService.GetUserChannelsAsync();
|
||||||
|
|
||||||
|
if (!userChannelsResult.IsSuccess || !userChannelsResult.Value.Any())
|
||||||
{
|
{
|
||||||
Id = "video1",
|
return View(new List<ChannelVideosViewModel>());
|
||||||
Title = "Como usar o PostAll - Tutorial",
|
|
||||||
Description = "Aprenda a usar todas as funcionalidades do PostAll para gerenciar suas redes sociais.",
|
|
||||||
ThumbnailUrl = "https://i.ytimg.com/vi/sample1/maxresdefault.jpg",
|
|
||||||
PublishedAt = DateTime.Now.AddDays(-2)
|
|
||||||
},
|
|
||||||
new VideoViewModel
|
|
||||||
{
|
|
||||||
Id = "video2",
|
|
||||||
Title = "Estratégias de Marketing Digital para 2024",
|
|
||||||
Description = "Conheça as melhores estratégias para alavancar seu negócio nas redes sociais em 2024.",
|
|
||||||
ThumbnailUrl = "https://i.ytimg.com/vi/sample2/maxresdefault.jpg",
|
|
||||||
PublishedAt = DateTime.Now.AddDays(-5)
|
|
||||||
},
|
|
||||||
new VideoViewModel
|
|
||||||
{
|
|
||||||
Id = "video3",
|
|
||||||
Title = "Análise de Métricas nas Redes Sociais",
|
|
||||||
Description = "Aprenda a interpretar as métricas das suas redes sociais e tomar decisões baseadas em dados.",
|
|
||||||
ThumbnailUrl = "https://i.ytimg.com/vi/sample3/maxresdefault.jpg",
|
|
||||||
PublishedAt = DateTime.Now.AddDays(-7)
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public IActionResult Index()
|
var channelsWithVideos = new List<ChannelVideosViewModel>();
|
||||||
{
|
|
||||||
var userVideos = GetSampleVideos();
|
|
||||||
return View(userVideos);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
// Para cada canal, obtém os vídeos associados
|
||||||
public IActionResult GetChannelVideos()
|
foreach (var channel in userChannelsResult.Value)
|
||||||
{
|
{
|
||||||
// Em um cenário real, você buscaria os vídeos da API do YouTube
|
var channelVideosResult = await _videoService.GetVideosByChannelIdAsync(channel.ChannelId);
|
||||||
var channelVideos = GetSampleVideos().OrderByDescending(v => v.PublishedAt).Take(10).ToList();
|
|
||||||
return PartialView("_ChannelVideosPartial", channelVideos);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
var channelVideos = new ChannelVideosViewModel
|
||||||
public IActionResult AddVideos(string[] selectedVideos)
|
{
|
||||||
{
|
Channel = channel,
|
||||||
if (selectedVideos == null || selectedVideos.Length == 0)
|
Videos = channelVideosResult.IsSuccess
|
||||||
|
? channelVideosResult.Value.Select(v => new VideoViewModel
|
||||||
|
{
|
||||||
|
Id = v.Id,
|
||||||
|
VideoId = v.VideoId,
|
||||||
|
ChannelId = v.ChannelId,
|
||||||
|
Title = v.Title,
|
||||||
|
Description = v.Description,
|
||||||
|
ThumbnailUrl = v.ThumbnailUrl,
|
||||||
|
PublishedAt = v.PublishedAt,
|
||||||
|
ViewCount = v.ViewCount,
|
||||||
|
LikeCount = v.LikeCount,
|
||||||
|
DislikeCount = v.DislikeCount,
|
||||||
|
CommentCount = v.CommentCount
|
||||||
|
}).ToList()
|
||||||
|
: new List<VideoViewModel>()
|
||||||
|
};
|
||||||
|
|
||||||
|
channelsWithVideos.Add(channelVideos);
|
||||||
|
}
|
||||||
|
|
||||||
|
return View(channelsWithVideos);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
TempData["Error"] = $"Erro ao carregar vídeos: {ex.Message}";
|
||||||
|
return View(new List<ChannelVideosViewModel>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exibe os vídeos disponíveis de um canal para serem adicionados
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GetChannelVideos(string channelId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var channelVideosResult = await _videoService.GetChannelVideosFromYouTubeAsync(channelId);
|
||||||
|
|
||||||
|
if (!channelVideosResult.IsSuccess)
|
||||||
|
{
|
||||||
|
|
||||||
|
return PartialView("_ChannelVideos", new List<VideoViewModel>());
|
||||||
|
}
|
||||||
|
|
||||||
|
var videosViewModel = channelVideosResult.Value.Select(v => new VideoViewModel
|
||||||
|
{
|
||||||
|
Id = v.Id,
|
||||||
|
VideoId = v.VideoId,
|
||||||
|
ChannelId = v.ChannelId,
|
||||||
|
Title = v.Title,
|
||||||
|
Description = v.Description,
|
||||||
|
ThumbnailUrl = v.ThumbnailUrl,
|
||||||
|
PublishedAt = v.PublishedAt,
|
||||||
|
ViewCount = v.ViewCount,
|
||||||
|
LikeCount = v.LikeCount,
|
||||||
|
DislikeCount = v.DislikeCount,
|
||||||
|
CommentCount = v.CommentCount
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
return PartialView("_ChannelVideos", videosViewModel);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Json(new { success = false, message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adiciona vários vídeos à lista do usuário
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> AddVideos(string channelId, List<string> selectedVideos)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(channelId))
|
||||||
|
{
|
||||||
|
TempData["Error"] = "ID do canal é obrigatório";
|
||||||
return RedirectToAction("Index");
|
return RedirectToAction("Index");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Em um cenário real, você salvaria esses IDs no banco de dados
|
if (selectedVideos == null || !selectedVideos.Any())
|
||||||
// Aqui apenas redirecionamos de volta para o Index
|
{
|
||||||
|
TempData["Error"] = "Selecione pelo menos um vídeo para adicionar";
|
||||||
|
return RedirectToAction("Index");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _videoService.AddVideosAsync(selectedVideos, channelId);
|
||||||
|
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
TempData["Message"] = "Vídeos adicionados com sucesso!";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TempData["Error"] = $"Erro ao adicionar vídeos: {result.Error.Description}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
TempData["Error"] = $"Erro ao adicionar vídeos: {ex.Message}";
|
||||||
|
}
|
||||||
|
|
||||||
TempData["Message"] = $"{selectedVideos.Length} vídeo(s) adicionado(s) com sucesso!";
|
|
||||||
return RedirectToAction("Index");
|
return RedirectToAction("Index");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Remove um vídeo da lista do usuário
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> Remove(string id, string channelId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(id))
|
||||||
|
{
|
||||||
|
TempData["Error"] = "ID do vídeo é obrigatório";
|
||||||
|
return RedirectToAction("Index");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _videoService.RemoveVideoAsync(id);
|
||||||
|
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
TempData["Message"] = "Vídeo removido com sucesso!";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TempData["Error"] = $"Erro ao remover vídeo: {result.Error.Description}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
TempData["Error"] = $"Erro ao remover vídeo: {ex.Message}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction("Index");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Busca vídeos no YouTube
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> Search(string query, string channelId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(query) || query.Length < 3)
|
||||||
|
{
|
||||||
|
return Json(new { success = false, message = "A consulta deve ter pelo menos 3 caracteres" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var results = await _videoService.SearchVideosAsync(query);
|
||||||
|
|
||||||
|
if (results.IsSuccess)
|
||||||
|
{
|
||||||
|
var videosViewModel = results.Value.Select(v => new VideoViewModel
|
||||||
|
{
|
||||||
|
VideoId = v.VideoId,
|
||||||
|
ChannelId = channelId, // Usado para saber a qual canal adicionar
|
||||||
|
Title = v.Title,
|
||||||
|
Description = v.Description,
|
||||||
|
ThumbnailUrl = v.ThumbnailUrl,
|
||||||
|
PublishedAt = v.PublishedAt,
|
||||||
|
ViewCount = v.ViewCount,
|
||||||
|
LikeCount = v.LikeCount,
|
||||||
|
DislikeCount = v.DislikeCount,
|
||||||
|
CommentCount = v.CommentCount
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
return Json(new { success = true, data = videosViewModel });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Json(new { success = false, message = "Erro ao buscar vídeos" });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Json(new { success = false, message = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
11
Postall/Models/ChannelVideosViewModel.cs
Normal file
11
Postall/Models/ChannelVideosViewModel.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
using Postall.Domain.Dtos;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Postall.Models
|
||||||
|
{
|
||||||
|
public class ChannelVideosViewModel
|
||||||
|
{
|
||||||
|
public ChannelResponse Channel { get; set; }
|
||||||
|
public List<VideoViewModel> Videos { get; set; } = new List<VideoViewModel>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,20 +2,19 @@
|
|||||||
|
|
||||||
namespace Postall.Models
|
namespace Postall.Models
|
||||||
{
|
{
|
||||||
|
|
||||||
public class VideoViewModel
|
public class VideoViewModel
|
||||||
{
|
{
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
|
public string VideoId { get; set; }
|
||||||
|
public string ChannelId { get; set; }
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
public string Description { get; set; }
|
public string Description { get; set; }
|
||||||
public string ThumbnailUrl { get; set; }
|
public string ThumbnailUrl { get; set; }
|
||||||
public DateTime PublishedAt { get; set; }
|
public DateTime PublishedAt { get; set; }
|
||||||
public bool IsSelected { get; set; }
|
public string VideoUrl => $"https://www.youtube.com/watch?v={VideoId}";
|
||||||
|
public ulong ViewCount { get; set; }
|
||||||
// Propriedades adicionais que podem ser úteis no futuro
|
public ulong LikeCount { get; set; }
|
||||||
public string ChannelTitle { get; set; }
|
public ulong DislikeCount { get; set; }
|
||||||
public string ChannelId { get; set; }
|
public ulong CommentCount { get; set; }
|
||||||
public string VideoUrl => $"https://www.youtube.com/watch?v={Id}";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,13 +9,6 @@
|
|||||||
<DockerfileContext>.</DockerfileContext>
|
<DockerfileContext>.</DockerfileContext>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Remove="Views\CartHome\**" />
|
|
||||||
<Content Remove="Views\CartHome\**" />
|
|
||||||
<EmbeddedResource Remove="Views\CartHome\**" />
|
|
||||||
<None Remove="Views\CartHome\**" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.Facebook" Version="8.0.12" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.Facebook" Version="8.0.12" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="8.0.7" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="8.0.7" />
|
||||||
|
|||||||
@ -1,18 +1,87 @@
|
|||||||
@model List<Postall.Models.VideoViewModel>
|
@model List<Postall.Models.ChannelVideosViewModel>
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Gerenciador de Vídeos";
|
ViewData["Title"] = "Gerenciador de Vídeos";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Estilos para cards de vídeo na página principal */
|
||||||
|
.card-title-truncate {
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-desc-truncate {
|
||||||
|
height: 60px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Garantir que o thumbnail tenha tamanho consistente */
|
||||||
|
.video-thumbnail {
|
||||||
|
width: 100%;
|
||||||
|
height: 100px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Garantir altura consistente para os cards */
|
||||||
|
.video-card {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para o botão ler mais */
|
||||||
|
.read-more-btn {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #007bff;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-more-btn:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para o conteúdo expandido em largura total */
|
||||||
|
.description-expanded {
|
||||||
|
max-height: 150px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
border-top: 1px solid #dee2e6;
|
||||||
|
margin: 10px 5px 5px 5px; /* Margem negativa para alinhar com os limites do card */
|
||||||
|
display: none;
|
||||||
|
position: relative;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ajuste na posição do botão Fechar */
|
||||||
|
.card-body {
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ajuste para cards com descrição expandida */
|
||||||
|
.card-with-expanded-desc {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<h2>Meus Vídeos</h2>
|
<h2>Meus Vídeos</h2>
|
||||||
<p class="text-muted">Gerencie seus vídeos do YouTube</p>
|
<p class="text-muted">Gerencie seus vídeos do YouTube por canal</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-right">
|
<div class="col-md-4 text-right">
|
||||||
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#addVideosModal">
|
<a href="@Url.Action("Index", "Channels")" class="btn btn-outline-primary mr-2">
|
||||||
<i class="bi bi-plus-circle"></i> Adicionar Vídeos
|
<i class="bi bi-collection"></i> Gerenciar Canais
|
||||||
</button>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -26,65 +95,122 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="row">
|
@if (TempData["Error"] != null)
|
||||||
@if (Model != null && Model.Any())
|
{
|
||||||
{
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||||
foreach (var video in Model)
|
@TempData["Error"]
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (Model == null || !Model.Any())
|
||||||
|
{
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="bi bi-info-circle"></i> Você não possui nenhum canal com vídeos.
|
||||||
|
<a href="@Url.Action("Index", "Channels")">Clique aqui</a> para adicionar um canal primeiro.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="accordion" id="channelsAccordion">
|
||||||
|
@foreach (var channelVideos in Model)
|
||||||
{
|
{
|
||||||
<div class="col-md-6 mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card">
|
<div class="card-header" id="heading-@channelVideos.Channel.Id">
|
||||||
<div class="card-header bg-transparent">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<h5 class="mb-0">@video.Title</h5>
|
<img src="@channelVideos.Channel.ThumbnailUrl" alt="@channelVideos.Channel.Title" class="img-thumbnail mr-3" style="width: 50px; height: 50px;">
|
||||||
<button class="btn btn-sm btn-link" type="button" data-toggle="collapse"
|
<h5 class="mb-0">@channelVideos.Channel.Title</h5>
|
||||||
data-target="#collapse-@video.Id" aria-expanded="false">
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-link" type="button" data-toggle="collapse"
|
||||||
|
data-target="#collapse-@channelVideos.Channel.Id" aria-expanded="true"
|
||||||
|
aria-controls="collapse-@channelVideos.Channel.Id">
|
||||||
<i class="bi bi-chevron-down"></i>
|
<i class="bi bi-chevron-down"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="btn btn-primary btn-sm"
|
||||||
|
onclick="loadChannelVideos('@channelVideos.Channel.ChannelId')"
|
||||||
|
data-toggle="modal" data-target="#addVideosModal">
|
||||||
|
<i class="bi bi-plus-circle"></i> Adicionar Vídeos
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="collapse-@channelVideos.Channel.Id" class="collapse show"
|
||||||
|
aria-labelledby="heading-@channelVideos.Channel.Id" data-parent="#channelsAccordion">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row">
|
@if (channelVideos.Videos != null && channelVideos.Videos.Any())
|
||||||
<div class="col-md-5">
|
{
|
||||||
<img src="@video.ThumbnailUrl" alt="@video.Title" class="img-fluid rounded">
|
<div class="row">
|
||||||
|
@foreach (var video in channelVideos.Videos)
|
||||||
|
{
|
||||||
|
<div class="col-md-6 mb-4">
|
||||||
|
<div class="card video-card">
|
||||||
|
<div class="card-header bg-transparent">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="card-title-truncate">@video.Title</h5>
|
||||||
|
<button class="btn btn-sm btn-link" type="button" data-toggle="collapse"
|
||||||
|
data-target="#videoCollapse-@video.Id" aria-expanded="false">
|
||||||
|
<i class="bi bi-chevron-down"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-5">
|
||||||
|
<img src="@video.ThumbnailUrl" alt="@video.Title" class="img-fluid rounded video-thumbnail">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-7">
|
||||||
|
<div>
|
||||||
|
<p class="card-desc-truncate mb-0">@video.Description</p>
|
||||||
|
<span class="read-more-btn" data-target="main-desc-@video.Id">Ler mais</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted small mt-2">Publicado em: @video.PublishedAt.ToString("dd/MM/yyyy")</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="main-desc-@video.Id" class="description-expanded">
|
||||||
|
<p><b>@video.Title</b></p>
|
||||||
|
@video.Description
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="collapse" id="videoCollapse-@video.Id">
|
||||||
|
<div class="card-footer bg-white">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<a href="@video.VideoUrl" target="_blank" class="btn btn-sm btn-outline-primary">
|
||||||
|
<i class="bi bi-youtube"></i> Ver no YouTube
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-sm btn-outline-danger"
|
||||||
|
onclick="removeVideo('@video.Id', '@channelVideos.Channel.Id')">
|
||||||
|
<i class="bi bi-trash"></i> Remover
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-7">
|
}
|
||||||
<p class="card-text">@(video.Description?.Length > 100 ? video.Description.Substring(0, 100) + "..." : video.Description)</p>
|
else
|
||||||
<p class="text-muted small">Publicado em: @video.PublishedAt.ToString("dd/MM/yyyy")</p>
|
{
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="bi bi-info-circle"></i> Este canal não possui vídeos adicionados.
|
||||||
|
Clique em "Adicionar Vídeos" para começar.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
</div>
|
|
||||||
<div class="collapse" id="collapse-@video.Id">
|
|
||||||
<div class="card-footer bg-white">
|
|
||||||
<div class="d-flex justify-content-between">
|
|
||||||
<div>
|
|
||||||
<a href="@video.VideoUrl" target="_blank" class="btn btn-sm btn-outline-primary">
|
|
||||||
<i class="bi bi-youtube"></i> Ver no YouTube
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button class="btn btn-sm btn-outline-secondary mr-1">
|
|
||||||
<i class="bi bi-pencil"></i> Editar
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-sm btn-outline-danger">
|
|
||||||
<i class="bi bi-trash"></i> Remover
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
</div>
|
||||||
else
|
}
|
||||||
{
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="alert alert-info">
|
|
||||||
<i class="bi bi-info-circle"></i> Você ainda não possui vídeos. Clique em "Adicionar Vídeos" para começar.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal para adicionar vídeos -->
|
<!-- Modal para adicionar vídeos -->
|
||||||
@ -92,7 +218,7 @@
|
|||||||
<div class="modal-dialog modal-lg" role="document">
|
<div class="modal-dialog modal-lg" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="addVideosModalLabel">Adicionar Vídeos do YouTube</h5>
|
<h5 class="modal-title" id="addVideosModalLabel">Adicionar Vídeos do Canal</h5>
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true">×</span>
|
||||||
</button>
|
</button>
|
||||||
@ -115,28 +241,69 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Formulário oculto para remover vídeos -->
|
||||||
|
<form id="removeVideoForm" method="post" action="@Url.Action("Remove", "Videos")">
|
||||||
|
<input type="hidden" id="videoIdToRemove" name="id">
|
||||||
|
<input type="hidden" id="channelIdForRemove" name="channelId">
|
||||||
|
</form>
|
||||||
|
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(function () {
|
// Variável para armazenar o ID do canal atual
|
||||||
// Carrega os vídeos do canal quando o modal é aberto
|
let currentChannelId = '';
|
||||||
$('#addVideosModal').on('shown.bs.modal', function () {
|
|
||||||
loadChannelVideos();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Função para carregar os vídeos do canal
|
// Função para carregar os vídeos do canal
|
||||||
function loadChannelVideos() {
|
function loadChannelVideos(channelId) {
|
||||||
$.ajax({
|
currentChannelId = channelId;
|
||||||
url: '@Url.Action("GetChannelVideos", "Videos")',
|
|
||||||
type: 'GET',
|
$.ajax({
|
||||||
success: function (result) {
|
url: '@Url.Action("GetChannelVideos", "Videos")',
|
||||||
$('#channelVideosContainer').html(result);
|
type: 'GET',
|
||||||
},
|
data: { channelId: channelId },
|
||||||
error: function (error) {
|
success: function (result) {
|
||||||
$('#channelVideosContainer').html('<div class="alert alert-danger">Erro ao carregar vídeos. Tente novamente.</div>');
|
$('#channelVideosContainer').html(result);
|
||||||
console.error('Erro:', error);
|
|
||||||
}
|
// Inicializa os botões "Ler mais" após carregar o conteúdo
|
||||||
});
|
setTimeout(function() {
|
||||||
|
initReadMoreButtons();
|
||||||
|
}, 200);
|
||||||
|
},
|
||||||
|
error: function (error) {
|
||||||
|
$('#channelVideosContainer').html('<div class="alert alert-danger">Erro ao carregar vídeos. Tente novamente.</div>');
|
||||||
|
console.error('Erro:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Função para remover um vídeo
|
||||||
|
function removeVideo(videoId, channelId) {
|
||||||
|
if (confirm('Tem certeza que deseja remover este vídeo?')) {
|
||||||
|
$('#videoIdToRemove').val(videoId);
|
||||||
|
$('#channelIdForRemove').val(channelId);
|
||||||
|
$('#removeVideoForm').submit();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Função para inicializar os botões "Ler mais"
|
||||||
|
function initReadMoreButtons() {
|
||||||
|
$('.read-more-btn').off('click').on('click', function() {
|
||||||
|
var targetId = $(this).data('target');
|
||||||
|
$('#' + targetId).slideToggle(200);
|
||||||
|
|
||||||
|
// Alternar texto do botão
|
||||||
|
var btnText = $(this).text() === 'Ler mais' ? 'Fechar' : 'Ler mais';
|
||||||
|
$(this).text(btnText);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(function () {
|
||||||
|
// Inicializar botões "Ler mais" na carga da página
|
||||||
|
initReadMoreButtons();
|
||||||
|
|
||||||
|
// Reinicializar botões quando um accordion é aberto
|
||||||
|
$('.collapse').on('shown.bs.collapse', function () {
|
||||||
|
initReadMoreButtons();
|
||||||
|
});
|
||||||
|
|
||||||
// Manipula o clique no botão de adicionar vídeos selecionados
|
// Manipula o clique no botão de adicionar vídeos selecionados
|
||||||
$('#btnAddSelectedVideos').click(function () {
|
$('#btnAddSelectedVideos').click(function () {
|
||||||
@ -155,6 +322,9 @@
|
|||||||
// Cria um formulário para enviar os IDs dos vídeos selecionados
|
// Cria um formulário para enviar os IDs dos vídeos selecionados
|
||||||
var form = $('<form action="@Url.Action("AddVideos", "Videos")" method="post"></form>');
|
var form = $('<form action="@Url.Action("AddVideos", "Videos")" method="post"></form>');
|
||||||
|
|
||||||
|
// Adiciona o ID do canal
|
||||||
|
form.append('<input type="hidden" name="channelId" value="' + currentChannelId + '">');
|
||||||
|
|
||||||
// Adiciona inputs ocultos para cada vídeo selecionado
|
// Adiciona inputs ocultos para cada vídeo selecionado
|
||||||
selectedVideos.forEach(function (videoId) {
|
selectedVideos.forEach(function (videoId) {
|
||||||
form.append('<input type="hidden" name="selectedVideos" value="' + videoId + '">');
|
form.append('<input type="hidden" name="selectedVideos" value="' + videoId + '">');
|
||||||
|
|||||||
136
Postall/Views/Videos/_ChannelVideos.cshtml
Normal file
136
Postall/Views/Videos/_ChannelVideos.cshtml
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
@model List<Postall.Models.VideoViewModel>
|
||||||
|
|
||||||
|
@if (Model == null || !Model.Any())
|
||||||
|
{
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="bi bi-info-circle"></i> Não há vídeos disponíveis para este canal.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="form-group mb-3">
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="videoSearchInput" class="form-control" placeholder="Filtrar vídeos...">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Estilos para os cards de vídeo */
|
||||||
|
.video-item .card {
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-item .video-image {
|
||||||
|
width: 120px;
|
||||||
|
height: 67px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
/* Estilo para o título do vídeo */
|
||||||
|
.video-title {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
/* Estilo para a descrição do vídeo */
|
||||||
|
.video-description {
|
||||||
|
height: 40px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
/* Estilo para o botão ler mais */
|
||||||
|
.read-more-btn {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #007bff;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-more-btn:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
/* Estilo para o conteúdo expandido */
|
||||||
|
.description-expanded {
|
||||||
|
max-height: 150px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
border-top: 1px solid #dee2e6;
|
||||||
|
margin: 10px -20px -20px -20px; /* Margem negativa para alinhar com os limites do card */
|
||||||
|
display: none;
|
||||||
|
position: relative;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ajuste na posição do botão Fechar */
|
||||||
|
.card-body {
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="row" id="videosList">
|
||||||
|
@foreach (var video in Model)
|
||||||
|
{
|
||||||
|
<div class="col-md-6 mb-3 video-item">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body p-3">
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="flex-shrink-0 mr-3">
|
||||||
|
<img src="@video.ThumbnailUrl" alt="@video.Title" class="img-thumbnail video-image">
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1 overflow-hidden">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input video-checkbox" type="checkbox" value="@video.VideoId" id="video-@video.VideoId">
|
||||||
|
<label class="form-check-label w-100" for="video-@video.VideoId">
|
||||||
|
<h6 class="mb-1 video-title">@video.Title</h6>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted small mb-1">Publicado em: @video.PublishedAt.ToString("dd/MM/yyyy")</p>
|
||||||
|
<div>
|
||||||
|
<p class="small video-description mb-0">@video.Description</p>
|
||||||
|
<span class="read-more-btn" data-target="desc-@video.VideoId">Ler mais</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="desc-@video.VideoId" class="description-expanded">
|
||||||
|
<p><b>@video.Title</b></p>
|
||||||
|
@video.Description
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(function() {
|
||||||
|
// Manipular clique no botão "Ler mais"
|
||||||
|
$('.read-more-btn').click(function() {
|
||||||
|
var targetId = $(this).data('target');
|
||||||
|
$('#' + targetId).slideToggle(200);
|
||||||
|
|
||||||
|
// Alternar texto do botão
|
||||||
|
var btnText = $(this).text() === 'Ler mais' ? 'Fechar' : 'Ler mais';
|
||||||
|
$(this).text(btnText);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filtro de vídeos
|
||||||
|
$('#videoSearchInput').on('keyup', function() {
|
||||||
|
var value = $(this).val().toLowerCase();
|
||||||
|
$('.video-item').filter(function() {
|
||||||
|
var title = $(this).find('.video-title').text().toLowerCase();
|
||||||
|
$(this).toggle(title.indexOf(value) > -1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
||||||
@ -1,51 +0,0 @@
|
|||||||
@model List<Postall.Models.VideoViewModel>
|
|
||||||
|
|
||||||
<form id="channelVideosForm">
|
|
||||||
<h6 class="mb-3">Últimos 10 vídeos disponíveis (ordenados por data):</h6>
|
|
||||||
|
|
||||||
@if (Model != null && Model.Any())
|
|
||||||
{
|
|
||||||
<div class="list-group">
|
|
||||||
@foreach (var video in Model)
|
|
||||||
{
|
|
||||||
<div class="list-group-item list-group-item-action flex-column align-items-start">
|
|
||||||
<div class="d-flex">
|
|
||||||
<div class="checkbox-container mr-3 mt-1">
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input video-checkbox" type="checkbox" value="@video.Id" id="video-@video.Id">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row w-100">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<img src="@video.ThumbnailUrl" alt="@video.Title" class="img-fluid rounded">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-8">
|
|
||||||
<div class="d-flex w-100 justify-content-between">
|
|
||||||
<h5 class="mb-1">@video.Title</h5>
|
|
||||||
<small>@video.PublishedAt.ToString("dd/MM/yyyy")</small>
|
|
||||||
</div>
|
|
||||||
<p class="mb-1">@video.Description</p>
|
|
||||||
<small class="text-muted">
|
|
||||||
<i class="bi bi-youtube"></i>
|
|
||||||
@(video.ChannelTitle ?? "Seu Canal")
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="alert alert-info">
|
|
||||||
Nenhum vídeo encontrado nos canais aos quais você tem acesso.
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.checkbox-container {
|
|
||||||
min-width: 30px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -8,6 +8,8 @@
|
|||||||
"MongoDbSettings": {
|
"MongoDbSettings": {
|
||||||
"ConnectionString": "mongodb://localhost:27017",
|
"ConnectionString": "mongodb://localhost:27017",
|
||||||
"DatabaseName": "YouTubeChannelsDB",
|
"DatabaseName": "YouTubeChannelsDB",
|
||||||
"ChannelsCollectionName": "Channels"
|
"ChannelsCollectionName": "Channels",
|
||||||
|
"VideosCollectionName": "Videos",
|
||||||
|
"UserSocialCollectionName": "UserSocial"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,8 @@
|
|||||||
"MongoDbSettings": {
|
"MongoDbSettings": {
|
||||||
"ConnectionString": "mongodb://localhost:27017",
|
"ConnectionString": "mongodb://localhost:27017",
|
||||||
"DatabaseName": "YouTubeChannelsDB",
|
"DatabaseName": "YouTubeChannelsDB",
|
||||||
"ChannelsCollectionName": "Channels"
|
"ChannelsCollectionName": "Channels",
|
||||||
|
"VideosCollectionName": "Videos",
|
||||||
|
"UserSocialCollectionName": "UserSocial"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user