Compare commits

...

3 Commits

Author SHA1 Message Date
Ricardo Carneiro
058b38a664 feat: Ajuste de repository para dados do usuario 2025-03-25 12:04:44 -03:00
Ricardo Carneiro
fa8aa0b610 feat:listanod videos e adicionando ao usuário 2025-03-08 19:13:24 -03:00
Ricardo Carneiro
a32c3c9eb9 feat: canais e listagem de canais com mongodb 2025-03-07 08:52:23 -03:00
33 changed files with 1812 additions and 217 deletions

View File

@ -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);
} }
} }

View File

@ -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));
}
} }
} }

View File

@ -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

View File

@ -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);
}
}

View 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);
}
}

View File

@ -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,

View 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
};
}
}
}

View File

@ -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,

View 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; }
}
}

View File

@ -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;
} }

View File

@ -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);

View 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);
}
}

View 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);
}
}

View 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;
}
}
}

View File

@ -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;
} }

View File

@ -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;
} }

View 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);
}
}
}

View 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();
}
}
}

View File

@ -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; }
} }
} }

View File

@ -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;
} }
} }

View File

@ -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();
}
} }
} }

View 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);
}
}
}
}

View File

@ -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;

View File

@ -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)
{ {

View File

@ -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 });
}
}
} }
} }

View 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>();
}
}

View File

@ -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}";
} }
} }

View File

@ -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" />

View File

@ -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">&times;</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">&times;</span> <span aria-hidden="true">&times;</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 + '">');

View 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>
}

View File

@ -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>

View File

@ -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"
} }
} }

View File

@ -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"
} }
} }