feat:listanod videos e adicionando ao usuário

This commit is contained in:
Ricardo Carneiro 2025-03-08 19:13:24 -03:00
parent a32c3c9eb9
commit fa8aa0b610
26 changed files with 1475 additions and 190 deletions

View File

@ -29,5 +29,6 @@ namespace BaseDomain.Results
public static Result Success() => new(true, 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);
}
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.Threading.Tasks;
namespace Postall.Domain
namespace Postall.Domain.Contracts.Repositories
{
/// <summary>
/// Interface para repositório de canais do YouTube no MongoDB

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

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

@ -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 Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Postall.Domain;
using Postall.Domain.Contracts.Repositories;
using Postall.Domain.Dtos;
using Postall.Domain.Entities;
using Postall.Domain.Services.Contracts;

View File

@ -7,7 +7,7 @@ using System.Threading.Tasks;
namespace Postall.Domain.Services.Contracts
{
public interface IVideoService
public interface IYouTubeServiceChannel
{
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 Postall.Domain;
using Postall.Domain.Contracts.Repositories;
using Postall.Domain.Services.Contracts;
using Postall.Infra.MongoDB.Repositories;
using Postall.Infra.Services;
@ -16,6 +17,7 @@ namespace Postall.Infra.MongoDB.Extensions
public static IServiceCollection AddRepositories(this IServiceCollection services)
{
services.AddScoped<IChannelRepository, ChannelRepository>();
services.AddScoped<IVideoRepository, VideoRepository>();
return services;
}

View File

@ -1,7 +1,7 @@
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;
using Postall.Domain;
using Postall.Domain.Contracts.Repositories;
using Postall.Domain.Dtos;
using Postall.Domain.Entities;
using Postall.Infra.MongoDB.Settings;

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,6 @@ namespace Postall.Infra.MongoDB.Settings
public string ConnectionString { get; set; }
public string DatabaseName { get; set; }
public string ChannelsCollectionName { get; set; }
public string VideosCollectionName { get; set; }
}
}

View File

@ -11,12 +11,16 @@ namespace BaseDomain.Extensions
{
services.AddHttpContextAccessor();
services.AddScoped<IVideoService, YouTubeServiceVideo>();
services.AddScoped<IYouTubeServiceChannel, YouTubeServiceChannel>();
services.AddScoped<IChannelService, ChannelVideoService>();
services.AddScoped<IChannelYoutubeService, ChannelYoutubeService>();
services.AddScoped<IVideoYoutubeService, VideoYoutubeService>();
services.AddScoped<IVideoService, VideoService>();
return services;
}
}

View File

@ -18,10 +18,17 @@ namespace Postall.Infra.Services
private readonly IConfiguration _config;
private readonly HttpClient _httpClient;
public FacebookTokenService(IConfiguration configuration, IMongoCollection<UserSocialData> tokens, IHttpClientFactory httpClientFactory)
{
_config = configuration;
_tokens = tokens;
_httpClient = httpClientFactory.CreateClient("Facebook");
}
public async Task<string> GetLongLivedToken(string shortLivedToken)
{
var appId = _config["Authentication:Facebook:AppId"];
var appSecret = _config["Authentication:Facebook:AppSecret"];
var appId = _config.GetSection("Authentication:Facebook:AppId").Value;
var appSecret = _config.GetSection("Authentication:Facebook:AppSecret").Value;
var response = await _httpClient.GetFromJsonAsync<FacebookTokenResponse>(
$"https://graph.facebook.com/oauth/access_token?" +

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
{
public class YouTubeServiceVideo: IVideoService
public class YouTubeServiceChannel: IYouTubeServiceChannel
{
private readonly IConfiguration _configuration;
private readonly IHttpContextAccessor _httpContextAccessor;
@ -16,7 +16,7 @@ namespace Postall.Infra.Services
private readonly string _clientId;
private readonly string _clientSecret;
public YouTubeServiceVideo(IConfiguration configuration, IHttpContextAccessor httpContextAccessor)
public YouTubeServiceChannel(IConfiguration configuration, IHttpContextAccessor httpContextAccessor)
{
_configuration = configuration;
_httpContextAccessor = httpContextAccessor;

View File

@ -1,75 +1,232 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Postall.Domain.Services.Contracts;
using Postall.Models;
using System.Security.Claims;
using System.Threading.Tasks;
using System;
using System.Collections.Generic;
using Postall.Models;
using System.Linq;
using System.Threading.Tasks;
namespace Postall.Controllers
{
[Authorize]
public class VideosController : Controller
{
// Simulação de dados para exemplo - em produção, estes dados viriam de uma API ou banco de dados
private List<VideoViewModel> GetSampleVideos()
private readonly IVideoService _videoService;
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",
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)
return View(new List<ChannelVideosViewModel>());
}
};
}
public IActionResult Index()
{
var userVideos = GetSampleVideos();
return View(userVideos);
}
var channelsWithVideos = new List<ChannelVideosViewModel>();
[HttpGet]
public IActionResult GetChannelVideos()
{
// Em um cenário real, você buscaria os vídeos da API do YouTube
var channelVideos = GetSampleVideos().OrderByDescending(v => v.PublishedAt).Take(10).ToList();
return PartialView("_ChannelVideosPartial", channelVideos);
}
// Para cada canal, obtém os vídeos associados
foreach (var channel in userChannelsResult.Value)
{
var channelVideosResult = await _videoService.GetVideosByChannelIdAsync(channel.ChannelId);
[HttpPost]
public IActionResult AddVideos(string[] selectedVideos)
{
if (selectedVideos == null || selectedVideos.Length == 0)
var channelVideos = new ChannelVideosViewModel
{
Channel = channel,
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");
}
// Em um cenário real, você salvaria esses IDs no banco de dados
// Aqui apenas redirecionamos de volta para o Index
if (selectedVideos == null || !selectedVideos.Any())
{
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");
}
/// <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
{
public class VideoViewModel
{
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 bool IsSelected { get; set; }
// Propriedades adicionais que podem ser úteis no futuro
public string ChannelTitle { get; set; }
public string ChannelId { get; set; }
public string VideoUrl => $"https://www.youtube.com/watch?v={Id}";
public string VideoUrl => $"https://www.youtube.com/watch?v={VideoId}";
public ulong ViewCount { get; set; }
public ulong LikeCount { get; set; }
public ulong DislikeCount { get; set; }
public ulong CommentCount { get; set; }
}
}
}

View File

@ -1,18 +1,87 @@
@model List<Postall.Models.VideoViewModel>
@model List<Postall.Models.ChannelVideosViewModel>
@{
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="row mb-4">
<div class="col-md-8">
<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 class="col-md-4 text-right">
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#addVideosModal">
<i class="bi bi-plus-circle"></i> Adicionar Vídeos
</button>
<a href="@Url.Action("Index", "Channels")" class="btn btn-outline-primary mr-2">
<i class="bi bi-collection"></i> Gerenciar Canais
</a>
</div>
</div>
@ -26,65 +95,122 @@
</div>
}
<div class="row">
@if (Model != null && Model.Any())
{
foreach (var video in Model)
@if (TempData["Error"] != null)
{
<div class="alert alert-danger alert-dismissible fade show" role="alert">
@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">
<div class="card-header bg-transparent">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0">@video.Title</h5>
<button class="btn btn-sm btn-link" type="button" data-toggle="collapse"
data-target="#collapse-@video.Id" aria-expanded="false">
<div class="card mb-4">
<div class="card-header" id="heading-@channelVideos.Channel.Id">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<img src="@channelVideos.Channel.ThumbnailUrl" alt="@channelVideos.Channel.Title" class="img-thumbnail mr-3" style="width: 50px; height: 50px;">
<h5 class="mb-0">@channelVideos.Channel.Title</h5>
</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>
</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 id="collapse-@channelVideos.Channel.Id" class="collapse show"
aria-labelledby="heading-@channelVideos.Channel.Id" data-parent="#channelsAccordion">
<div class="card-body">
<div class="row">
<div class="col-md-5">
<img src="@video.ThumbnailUrl" alt="@video.Title" class="img-fluid rounded">
@if (channelVideos.Videos != null && channelVideos.Videos.Any())
{
<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 class="col-md-7">
<p class="card-text">@(video.Description?.Length > 100 ? video.Description.Substring(0, 100) + "..." : video.Description)</p>
<p class="text-muted small">Publicado em: @video.PublishedAt.ToString("dd/MM/yyyy")</p>
}
else
{
<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 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>
}
}
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 -->
@ -92,7 +218,7 @@
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<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">
<span aria-hidden="true">&times;</span>
</button>
@ -115,28 +241,69 @@
</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 {
<script type="text/javascript">
$(function () {
// Carrega os vídeos do canal quando o modal é aberto
$('#addVideosModal').on('shown.bs.modal', function () {
loadChannelVideos();
});
// Variável para armazenar o ID do canal atual
let currentChannelId = '';
// Função para carregar os vídeos do canal
function loadChannelVideos() {
$.ajax({
url: '@Url.Action("GetChannelVideos", "Videos")',
type: 'GET',
success: function (result) {
$('#channelVideosContainer').html(result);
},
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 carregar os vídeos do canal
function loadChannelVideos(channelId) {
currentChannelId = channelId;
$.ajax({
url: '@Url.Action("GetChannelVideos", "Videos")',
type: 'GET',
data: { channelId: channelId },
success: function (result) {
$('#channelVideosContainer').html(result);
// 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
$('#btnAddSelectedVideos').click(function () {
@ -155,6 +322,9 @@
// Cria um formulário para enviar os IDs dos vídeos selecionados
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
selectedVideos.forEach(function (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

@ -28,6 +28,7 @@
"MongoDbSettings": {
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "YouTubeChannelsDB",
"ChannelsCollectionName": "Channels"
"ChannelsCollectionName": "Channels",
"VideosCollectionName": "Videos"
}
}