generated from ricardo/MVCLogin
feat:listanod videos e adicionando ao usuário
This commit is contained in:
parent
a32c3c9eb9
commit
fa8aa0b610
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
23
Postall.Domain/Contracts/Repositories/IVideoRepository.cs
Normal file
23
Postall.Domain/Contracts/Repositories/IVideoRepository.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using Postall.Domain.Entities;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Postall.Domain
|
||||
{
|
||||
public interface IVideoRepository
|
||||
{
|
||||
Task<IEnumerable<VideoData>> GetAllAsync();
|
||||
Task<VideoData> GetByIdAsync(string id);
|
||||
Task<VideoData> GetByVideoIdAsync(string videoId);
|
||||
Task<IList<VideoData>> GetByUserIdAsync(string userId);
|
||||
Task<IList<VideoData>> GetByChannelIdAsync(string channelId);
|
||||
Task<IList<VideoData>> GetByUserIdAndChannelIdAsync(string userId, string channelId);
|
||||
Task<VideoData> GetByUserIdAndVideoIdAsync(string userId, string videoId);
|
||||
Task<VideoData> AddAsync(VideoData videoData);
|
||||
Task<IEnumerable<VideoData>> AddManyAsync(IEnumerable<VideoData> videos);
|
||||
Task<bool> UpdateAsync(VideoData videoData);
|
||||
Task<bool> DeleteAsync(string id);
|
||||
Task<bool> DeleteByVideoIdAsync(string videoId);
|
||||
Task<IEnumerable<VideoData>> SearchAsync(string searchTerm);
|
||||
}
|
||||
}
|
||||
65
Postall.Domain/Dtos/VideoResponse.cs
Normal file
65
Postall.Domain/Dtos/VideoResponse.cs
Normal file
@ -0,0 +1,65 @@
|
||||
using Postall.Domain.Entities;
|
||||
using System;
|
||||
|
||||
namespace Postall.Domain.Dtos
|
||||
{
|
||||
public class VideoResponse
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string VideoId { get; set; }
|
||||
public string ChannelId { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string ThumbnailUrl { get; set; }
|
||||
public DateTime PublishedAt { get; set; }
|
||||
public ulong ViewCount { get; set; }
|
||||
public ulong LikeCount { get; set; }
|
||||
public ulong DislikeCount { get; set; }
|
||||
public ulong CommentCount { get; set; }
|
||||
}
|
||||
|
||||
public static class VideoDataExtensions
|
||||
{
|
||||
public static VideoResponse ToVideoResponse(this VideoData videoData)
|
||||
{
|
||||
if (videoData == null)
|
||||
return null;
|
||||
|
||||
return new VideoResponse
|
||||
{
|
||||
Id = videoData.Id,
|
||||
VideoId = videoData.VideoId,
|
||||
ChannelId = videoData.ChannelId,
|
||||
Title = videoData.Title,
|
||||
Description = videoData.Description,
|
||||
ThumbnailUrl = videoData.ThumbnailUrl,
|
||||
PublishedAt = videoData.PublishedAt,
|
||||
ViewCount = videoData.ViewCount,
|
||||
LikeCount = videoData.LikeCount,
|
||||
DislikeCount = videoData.DislikeCount,
|
||||
CommentCount = videoData.CommentCount
|
||||
};
|
||||
}
|
||||
|
||||
public static VideoData ToVideoData(this VideoResponse videoResponse)
|
||||
{
|
||||
if (videoResponse == null)
|
||||
return null;
|
||||
|
||||
return new VideoData
|
||||
{
|
||||
Id = videoResponse.Id,
|
||||
VideoId = videoResponse.VideoId,
|
||||
ChannelId = videoResponse.ChannelId,
|
||||
Title = videoResponse.Title,
|
||||
Description = videoResponse.Description,
|
||||
ThumbnailUrl = videoResponse.ThumbnailUrl,
|
||||
PublishedAt = videoResponse.PublishedAt,
|
||||
ViewCount = videoResponse.ViewCount,
|
||||
LikeCount = videoResponse.LikeCount,
|
||||
DislikeCount = videoResponse.DislikeCount,
|
||||
CommentCount = videoResponse.CommentCount
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
27
Postall.Domain/Entities/VideosData.cs
Normal file
27
Postall.Domain/Entities/VideosData.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Bson.Serialization.Attributes;
|
||||
|
||||
namespace Postall.Domain.Entities
|
||||
{
|
||||
public class VideoData
|
||||
{
|
||||
[BsonId]
|
||||
[BsonRepresentation(BsonType.ObjectId)]
|
||||
public string Id { get; set; }
|
||||
|
||||
public string UserId { get; set; }
|
||||
public string ChannelId { get; set; }
|
||||
public string VideoId { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string ThumbnailUrl { get; set; }
|
||||
public DateTime PublishedAt { get; set; }
|
||||
public ulong ViewCount { get; set; }
|
||||
public ulong LikeCount { get; set; }
|
||||
public ulong DislikeCount { get; set; }
|
||||
public ulong CommentCount { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
using BaseDomain.Results;
|
||||
using 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;
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
14
Postall.Domain/Services/Contracts/IVideoYoutubeService.cs
Normal file
14
Postall.Domain/Services/Contracts/IVideoYoutubeService.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using BaseDomain.Results;
|
||||
using Postall.Domain.Dtos;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Postall.Domain.Services.Contracts
|
||||
{
|
||||
public interface IVideoYoutubeService
|
||||
{
|
||||
Task<Result<VideoResponse>> GetVideoDetailsAsync(string videoId);
|
||||
Task<Result<List<VideoResponse>>> GetChannelVideosAsync(string channelId, int maxResults = 50);
|
||||
Task<Result<List<VideoResponse>>> SearchVideosAsync(string query, int maxResults = 10);
|
||||
}
|
||||
}
|
||||
19
Postall.Domain/Services/Contracts/IYouTubeServiceChannel.cs
Normal file
19
Postall.Domain/Services/Contracts/IYouTubeServiceChannel.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using BaseDomain.Results;
|
||||
using Postall.Domain.Dtos;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Postall.Domain.Services.Contracts
|
||||
{
|
||||
public interface IVideoService
|
||||
{
|
||||
Task<Result<List<VideoResponse>>> GetUserVideosAsync();
|
||||
Task<Result<List<VideoResponse>>> GetVideosByChannelIdAsync(string channelId);
|
||||
Task<Result<VideoResponse>> GetVideoDetailsAsync(string videoId);
|
||||
Task<Result<List<VideoResponse>>> GetChannelVideosFromYouTubeAsync(string channelId, int maxResults = 10);
|
||||
Task<Result<bool>> AddVideoAsync(string videoId, string channelId);
|
||||
Task<Result<bool>> AddVideosAsync(List<string> videoIds, string channelId);
|
||||
Task<Result<bool>> RemoveVideoAsync(string videoId);
|
||||
Task<Result<List<VideoResponse>>> SearchVideosAsync(string query, int maxResults = 10);
|
||||
}
|
||||
}
|
||||
308
Postall.Domain/Services/VideoService .cs
Normal file
308
Postall.Domain/Services/VideoService .cs
Normal file
@ -0,0 +1,308 @@
|
||||
using BaseDomain.Results;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Postall.Domain;
|
||||
using Postall.Domain.Contracts.Repositories;
|
||||
using Postall.Domain.Dtos;
|
||||
using Postall.Domain.Entities;
|
||||
using Postall.Domain.Services.Contracts;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Postall.Infra.Services
|
||||
{
|
||||
public class VideoService : IVideoService
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IVideoRepository _videoRepository;
|
||||
private readonly IChannelRepository _channelRepository;
|
||||
private readonly IVideoYoutubeService _videoYoutubeService;
|
||||
|
||||
public VideoService(
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IVideoRepository videoRepository,
|
||||
IChannelRepository channelRepository,
|
||||
IVideoYoutubeService videoYoutubeService)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_videoRepository = videoRepository;
|
||||
_channelRepository = channelRepository;
|
||||
_videoYoutubeService = videoYoutubeService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtém o ID do usuário atual a partir das claims
|
||||
/// </summary>
|
||||
private string GetCurrentUserId()
|
||||
{
|
||||
var user = _httpContextAccessor.HttpContext.User;
|
||||
var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
{
|
||||
var email = user.FindFirst(ClaimTypes.Email)?.Value;
|
||||
if (!string.IsNullOrEmpty(email))
|
||||
{
|
||||
userId = Convert.ToBase64String(
|
||||
System.Text.Encoding.UTF8.GetBytes(email)
|
||||
).Replace("/", "_").Replace("+", "-").Replace("=", "");
|
||||
}
|
||||
}
|
||||
|
||||
return userId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtém todos os vídeos do usuário atual
|
||||
/// </summary>
|
||||
public async Task<Result<List<VideoResponse>>> GetUserVideosAsync()
|
||||
{
|
||||
var userId = GetCurrentUserId();
|
||||
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
return new List<VideoResponse>();
|
||||
|
||||
var videos = await _videoRepository.GetByUserIdAsync(userId);
|
||||
return videos.Select(v => v.ToVideoResponse()).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtém os vídeos de um canal específico para o usuário atual
|
||||
/// </summary>
|
||||
public async Task<Result<List<VideoResponse>>> GetVideosByChannelIdAsync(string channelId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(channelId))
|
||||
return new List<VideoResponse>();
|
||||
|
||||
var userId = GetCurrentUserId();
|
||||
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
return new List<VideoResponse>();
|
||||
|
||||
var videos = await _videoRepository.GetByUserIdAndChannelIdAsync(userId, channelId);
|
||||
return videos.Select(v => v.ToVideoResponse()).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtém os detalhes de um vídeo específico
|
||||
/// </summary>
|
||||
public async Task<Result<VideoResponse>> GetVideoDetailsAsync(string videoId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(videoId))
|
||||
return Result<VideoResponse>.Failure("ID do vídeo inválido");
|
||||
|
||||
var videoDetails = await _videoYoutubeService.GetVideoDetailsAsync(videoId);
|
||||
return videoDetails;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtém os vídeos de um canal diretamente do YouTube
|
||||
/// </summary>
|
||||
public async Task<Result<List<VideoResponse>>> GetChannelVideosFromYouTubeAsync(string channelId, int maxResults = 10)
|
||||
{
|
||||
if (string.IsNullOrEmpty(channelId))
|
||||
return Result<List<VideoResponse>>.Failure("ID do canal inválido");
|
||||
|
||||
// Verifica se o canal existe para o usuário
|
||||
var userId = GetCurrentUserId();
|
||||
var channel = await _channelRepository.GetByUserIdAndChannelIdAsync(userId, channelId);
|
||||
|
||||
if (channel == null)
|
||||
return Result<List<VideoResponse>>.Failure("Canal não encontrado para este usuário");
|
||||
|
||||
// Obtém os vídeos que o usuário já adicionou para este canal
|
||||
var userVideos = await _videoRepository.GetByUserIdAndChannelIdAsync(userId, channelId);
|
||||
var userVideoIds = userVideos.Select(v => v.VideoId).ToHashSet();
|
||||
|
||||
// Busca os vídeos do canal no YouTube
|
||||
var channelVideos = await _videoYoutubeService.GetChannelVideosAsync(channelId, maxResults);
|
||||
|
||||
if (!channelVideos.IsSuccess)
|
||||
return channelVideos;
|
||||
|
||||
// Marca os vídeos que já foram adicionados pelo usuário
|
||||
foreach (var video in channelVideos.Value)
|
||||
{
|
||||
if (userVideoIds.Contains(video.VideoId))
|
||||
{
|
||||
// Esse vídeo já foi adicionado pelo usuário
|
||||
video.Id = userVideos.First(v => v.VideoId == video.VideoId).Id;
|
||||
}
|
||||
}
|
||||
|
||||
return channelVideos;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adiciona um vídeo à lista do usuário
|
||||
/// </summary>
|
||||
public async Task<Result<bool>> AddVideoAsync(string videoId, string channelId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(videoId))
|
||||
return Result<bool>.Failure("ID do vídeo inválido");
|
||||
|
||||
if (string.IsNullOrEmpty(channelId))
|
||||
return Result<bool>.Failure("ID do canal inválido");
|
||||
|
||||
var userId = GetCurrentUserId();
|
||||
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
return Result<bool>.Failure("Usuário não identificado");
|
||||
|
||||
// Verifica se o canal existe para o usuário
|
||||
var channel = await _channelRepository.GetByUserIdAndChannelIdAsync(userId, channelId);
|
||||
|
||||
if (channel == null)
|
||||
return Result<bool>.Failure("Canal não encontrado para este usuário");
|
||||
|
||||
// Verifica se o vídeo já existe para o usuário
|
||||
var existingVideo = await _videoRepository.GetByUserIdAndVideoIdAsync(userId, videoId);
|
||||
|
||||
if (existingVideo != null)
|
||||
return true; // Vídeo já existe, retorna sucesso
|
||||
|
||||
try
|
||||
{
|
||||
// Obtém os detalhes do vídeo do YouTube
|
||||
var videoDetails = await _videoYoutubeService.GetVideoDetailsAsync(videoId);
|
||||
|
||||
if (!videoDetails.IsSuccess)
|
||||
return Result<bool>.Failure(videoDetails.Error);
|
||||
|
||||
// Cria o objeto de dados do vídeo
|
||||
var videoData = new VideoData
|
||||
{
|
||||
UserId = userId,
|
||||
ChannelId = channelId,
|
||||
VideoId = videoId,
|
||||
Title = videoDetails.Value.Title,
|
||||
Description = videoDetails.Value.Description,
|
||||
ThumbnailUrl = videoDetails.Value.ThumbnailUrl,
|
||||
PublishedAt = videoDetails.Value.PublishedAt,
|
||||
ViewCount = videoDetails.Value.ViewCount,
|
||||
LikeCount = videoDetails.Value.LikeCount,
|
||||
DislikeCount = videoDetails.Value.DislikeCount,
|
||||
CommentCount = videoDetails.Value.CommentCount,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// Adiciona o vídeo ao repositório
|
||||
await _videoRepository.AddAsync(videoData);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<bool>.Failure(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adiciona múltiplos vídeos à lista do usuário
|
||||
/// </summary>
|
||||
public async Task<Result<bool>> AddVideosAsync(List<string> videoIds, string channelId)
|
||||
{
|
||||
if (videoIds == null || !videoIds.Any())
|
||||
return Result<bool>.Failure("Lista de IDs de vídeos inválida");
|
||||
|
||||
if (string.IsNullOrEmpty(channelId))
|
||||
return Result<bool>.Failure("ID do canal inválido");
|
||||
|
||||
var userId = GetCurrentUserId();
|
||||
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
return Result<bool>.Failure("Usuário não identificado");
|
||||
|
||||
// Verifica se o canal existe para o usuário
|
||||
var channel = await _channelRepository.GetByUserIdAndChannelIdAsync(userId, channelId);
|
||||
|
||||
if (channel == null)
|
||||
return Result<bool>.Failure("Canal não encontrado para este usuário");
|
||||
|
||||
// Obtém os vídeos que o usuário já adicionou
|
||||
var existingVideos = await _videoRepository.GetByUserIdAsync(userId);
|
||||
var existingVideoIds = existingVideos.Select(v => v.VideoId).ToHashSet();
|
||||
|
||||
// Filtra os vídeos que ainda não foram adicionados
|
||||
var newVideoIds = videoIds.Where(id => !existingVideoIds.Contains(id)).ToList();
|
||||
|
||||
if (newVideoIds.Count == 0)
|
||||
return true; // Todos os vídeos já existem
|
||||
|
||||
try
|
||||
{
|
||||
var videoDataList = new List<VideoData>();
|
||||
|
||||
// Obtém os detalhes de cada vídeo do YouTube e cria os objetos de dados
|
||||
foreach (var videoId in newVideoIds)
|
||||
{
|
||||
var videoDetails = await _videoYoutubeService.GetVideoDetailsAsync(videoId);
|
||||
|
||||
if (videoDetails.IsSuccess)
|
||||
{
|
||||
videoDataList.Add(new VideoData
|
||||
{
|
||||
UserId = userId,
|
||||
ChannelId = channelId,
|
||||
VideoId = videoId,
|
||||
Title = videoDetails.Value.Title,
|
||||
Description = videoDetails.Value.Description,
|
||||
ThumbnailUrl = videoDetails.Value.ThumbnailUrl,
|
||||
PublishedAt = videoDetails.Value.PublishedAt,
|
||||
ViewCount = videoDetails.Value.ViewCount,
|
||||
LikeCount = videoDetails.Value.LikeCount,
|
||||
DislikeCount = videoDetails.Value.DislikeCount,
|
||||
CommentCount = videoDetails.Value.CommentCount,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Adiciona os vídeos ao repositório
|
||||
if (videoDataList.Any())
|
||||
{
|
||||
await _videoRepository.AddManyAsync(videoDataList);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<bool>.Failure(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove um vídeo da lista do usuário
|
||||
/// </summary>
|
||||
public async Task<Result<bool>> RemoveVideoAsync(string videoId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(videoId))
|
||||
return Result<bool>.Failure("ID do vídeo inválido");
|
||||
|
||||
try
|
||||
{
|
||||
await _videoRepository.DeleteAsync(videoId);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<bool>.Failure(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Busca vídeos no YouTube pelo título ou descrição
|
||||
/// </summary>
|
||||
public async Task<Result<List<VideoResponse>>> SearchVideosAsync(string query, int maxResults = 10)
|
||||
{
|
||||
if (string.IsNullOrEmpty(query) || query.Length < 3)
|
||||
return new List<VideoResponse>();
|
||||
|
||||
var results = await _videoYoutubeService.SearchVideosAsync(query, maxResults);
|
||||
return results;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using 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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
253
Postall.Infra.MongoDB/Repositories/VideoRepository.cs
Normal file
253
Postall.Infra.MongoDB/Repositories/VideoRepository.cs
Normal file
@ -0,0 +1,253 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using MongoDB.Bson;
|
||||
using MongoDB.Driver;
|
||||
using Postall.Domain;
|
||||
using Postall.Domain.Entities;
|
||||
using Postall.Infra.MongoDB.Settings;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Postall.Infra.MongoDB.Repositories
|
||||
{
|
||||
public class VideoRepository : IVideoRepository
|
||||
{
|
||||
private readonly IMongoCollection<VideoData> _videosCollection;
|
||||
|
||||
public VideoRepository(IOptions<MongoDbSettings> mongoDbSettings)
|
||||
{
|
||||
var client = new MongoClient(mongoDbSettings.Value.ConnectionString);
|
||||
var database = client.GetDatabase(mongoDbSettings.Value.DatabaseName);
|
||||
_videosCollection = database.GetCollection<VideoData>(mongoDbSettings.Value.VideosCollectionName);
|
||||
|
||||
// Cria índice composto para videoId e userId
|
||||
var indexKeysDefinition = Builders<VideoData>.IndexKeys
|
||||
.Ascending(v => v.VideoId)
|
||||
.Ascending(v => v.UserId);
|
||||
_videosCollection.Indexes.CreateOne(new CreateIndexModel<VideoData>(indexKeysDefinition, new CreateIndexOptions { Unique = true }));
|
||||
|
||||
// Cria índice para buscas por texto
|
||||
var textIndexDefinition = Builders<VideoData>.IndexKeys
|
||||
.Text(v => v.Title)
|
||||
.Text(v => v.Description);
|
||||
_videosCollection.Indexes.CreateOne(new CreateIndexModel<VideoData>(textIndexDefinition));
|
||||
|
||||
// Cria índice para buscas por channelId
|
||||
var channelIndexDefinition = Builders<VideoData>.IndexKeys
|
||||
.Ascending(v => v.ChannelId);
|
||||
_videosCollection.Indexes.CreateOne(new CreateIndexModel<VideoData>(channelIndexDefinition));
|
||||
|
||||
// Cria índice para userId + channelId
|
||||
var userChannelIndexDefinition = Builders<VideoData>.IndexKeys
|
||||
.Ascending(v => v.UserId)
|
||||
.Ascending(v => v.ChannelId);
|
||||
_videosCollection.Indexes.CreateOne(new CreateIndexModel<VideoData>(userChannelIndexDefinition));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtém todos os vídeos
|
||||
/// </summary>
|
||||
public async Task<IEnumerable<VideoData>> GetAllAsync()
|
||||
{
|
||||
return await _videosCollection.Find(v => true).ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtém um vídeo pelo ID do MongoDB
|
||||
/// </summary>
|
||||
public async Task<VideoData> GetByIdAsync(string id)
|
||||
{
|
||||
if (!ObjectId.TryParse(id, out _))
|
||||
return null;
|
||||
|
||||
return await _videosCollection.Find(v => v.Id == id).FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtém um vídeo pelo ID do YouTube
|
||||
/// </summary>
|
||||
public async Task<VideoData> GetByVideoIdAsync(string videoId)
|
||||
{
|
||||
return await _videosCollection.Find(v => v.VideoId == videoId).FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtém os vídeos pelo ID do usuário
|
||||
/// </summary>
|
||||
public async Task<IList<VideoData>> GetByUserIdAsync(string userId)
|
||||
{
|
||||
return await _videosCollection.Find(v => v.UserId == userId).ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtém os vídeos pelo ID do canal
|
||||
/// </summary>
|
||||
public async Task<IList<VideoData>> GetByChannelIdAsync(string channelId)
|
||||
{
|
||||
return await _videosCollection.Find(v => v.ChannelId == channelId).ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtém os vídeos pelo ID do usuário e ID do canal
|
||||
/// </summary>
|
||||
public async Task<IList<VideoData>> GetByUserIdAndChannelIdAsync(string userId, string channelId)
|
||||
{
|
||||
return await _videosCollection.Find(v => v.UserId == userId && v.ChannelId == channelId).ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtém um vídeo pelo ID do usuário e ID do vídeo
|
||||
/// </summary>
|
||||
public async Task<VideoData> GetByUserIdAndVideoIdAsync(string userId, string videoId)
|
||||
{
|
||||
return await _videosCollection.Find(v => v.UserId == userId && v.VideoId == videoId).FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adiciona um novo vídeo
|
||||
/// </summary>
|
||||
public async Task<VideoData> AddAsync(VideoData videoData)
|
||||
{
|
||||
var existingVideo = await GetByUserIdAndVideoIdAsync(videoData.UserId, videoData.VideoId);
|
||||
if (existingVideo != null)
|
||||
return existingVideo;
|
||||
|
||||
if (string.IsNullOrEmpty(videoData.Id) || !ObjectId.TryParse(videoData.Id, out _))
|
||||
{
|
||||
videoData.Id = ObjectId.GenerateNewId().ToString();
|
||||
}
|
||||
|
||||
videoData.CreatedAt = DateTime.UtcNow;
|
||||
|
||||
try
|
||||
{
|
||||
await _videosCollection.InsertOneAsync(videoData);
|
||||
}
|
||||
catch (MongoWriteException ex)
|
||||
{
|
||||
if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
|
||||
{
|
||||
return await GetByUserIdAndVideoIdAsync(videoData.UserId, videoData.VideoId);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
return videoData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adiciona vários vídeos de uma vez
|
||||
/// </summary>
|
||||
public async Task<IEnumerable<VideoData>> AddManyAsync(IEnumerable<VideoData> videos)
|
||||
{
|
||||
if (videos == null || !videos.Any())
|
||||
return new List<VideoData>();
|
||||
|
||||
var videosList = videos.ToList();
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Gera novos IDs para o MongoDB se necessário
|
||||
foreach (var videoData in videosList)
|
||||
{
|
||||
if (string.IsNullOrEmpty(videoData.Id) || !ObjectId.TryParse(videoData.Id, out _))
|
||||
{
|
||||
videoData.Id = ObjectId.GenerateNewId().ToString();
|
||||
}
|
||||
videoData.CreatedAt = now;
|
||||
}
|
||||
|
||||
// Verifica vídeos existentes pelo ID do YouTube e usuário
|
||||
var userIds = videosList.Select(v => v.UserId).Distinct().ToList();
|
||||
var videoIds = videosList.Select(v => v.VideoId).ToList();
|
||||
|
||||
var filter = Builders<VideoData>.Filter.And(
|
||||
Builders<VideoData>.Filter.In(v => v.UserId, userIds),
|
||||
Builders<VideoData>.Filter.In(v => v.VideoId, videoIds)
|
||||
);
|
||||
|
||||
var existingVideos = await _videosCollection.Find(filter).ToListAsync();
|
||||
|
||||
// Filtra apenas os vídeos que ainda não foram adicionados
|
||||
var existingPairs = existingVideos
|
||||
.Select(v => $"{v.UserId}:{v.VideoId}")
|
||||
.ToHashSet();
|
||||
|
||||
var newVideos = videosList
|
||||
.Where(v => !existingPairs.Contains($"{v.UserId}:{v.VideoId}"))
|
||||
.ToList();
|
||||
|
||||
if (newVideos.Any())
|
||||
{
|
||||
try
|
||||
{
|
||||
await _videosCollection.InsertManyAsync(newVideos);
|
||||
}
|
||||
catch (MongoBulkWriteException)
|
||||
{
|
||||
// Em caso de erro, adiciona um por um
|
||||
foreach (var video in newVideos)
|
||||
{
|
||||
try { await AddAsync(video); }
|
||||
catch { /* Ignora erros individuais */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newVideos;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Atualiza um vídeo existente
|
||||
/// </summary>
|
||||
public async Task<bool> UpdateAsync(VideoData videoData)
|
||||
{
|
||||
if (string.IsNullOrEmpty(videoData.Id) || !ObjectId.TryParse(videoData.Id, out _))
|
||||
return false;
|
||||
|
||||
videoData.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
var result = await _videosCollection.ReplaceOneAsync(
|
||||
v => v.Id == videoData.Id,
|
||||
videoData,
|
||||
new ReplaceOptions { IsUpsert = false });
|
||||
|
||||
return result.IsAcknowledged && result.ModifiedCount > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove um vídeo pelo ID do MongoDB
|
||||
/// </summary>
|
||||
public async Task<bool> DeleteAsync(string id)
|
||||
{
|
||||
if (!ObjectId.TryParse(id, out _))
|
||||
return false;
|
||||
|
||||
var result = await _videosCollection.DeleteOneAsync(v => v.Id == id);
|
||||
return result.IsAcknowledged && result.DeletedCount > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove um vídeo pelo ID do YouTube
|
||||
/// </summary>
|
||||
public async Task<bool> DeleteByVideoIdAsync(string videoId)
|
||||
{
|
||||
var result = await _videosCollection.DeleteOneAsync(v => v.VideoId == videoId);
|
||||
return result.IsAcknowledged && result.DeletedCount > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Busca vídeos com base em um termo de pesquisa no título ou descrição
|
||||
/// </summary>
|
||||
public async Task<IEnumerable<VideoData>> SearchAsync(string searchTerm)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(searchTerm))
|
||||
return await GetAllAsync();
|
||||
|
||||
var filter = Builders<VideoData>.Filter.Text(searchTerm);
|
||||
return await _videosCollection.Find(filter).ToListAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -14,5 +14,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; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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?" +
|
||||
|
||||
134
Postall.Infra/Services/VideoYoutubeService .cs
Normal file
134
Postall.Infra/Services/VideoYoutubeService .cs
Normal file
@ -0,0 +1,134 @@
|
||||
using BaseDomain.Results;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Postall.Domain.Dtos;
|
||||
using Postall.Domain.Services.Contracts;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Postall.Infra.Services
|
||||
{
|
||||
public class VideoYoutubeService : IVideoYoutubeService
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly string _apiKey;
|
||||
|
||||
public VideoYoutubeService(IConfiguration configuration)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_apiKey = _configuration["YouTube:ApiKey"];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Busca os detalhes de um vídeo na API do YouTube
|
||||
/// </summary>
|
||||
public async Task<Result<VideoResponse>> GetVideoDetailsAsync(string videoId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var youtubeService = new YouTubeBaseServiceWrapper(_apiKey);
|
||||
|
||||
var videosRequest = youtubeService.Videos.List("snippet,statistics");
|
||||
videosRequest.Id = videoId;
|
||||
|
||||
var response = await videosRequest.ExecuteAsync();
|
||||
|
||||
if (response.Items.Count == 0)
|
||||
return Result<VideoResponse>.Failure($"Vídeo não encontrado: {videoId}");
|
||||
|
||||
var video = response.Items[0];
|
||||
var snippet = video.Snippet;
|
||||
var statistics = video.Statistics;
|
||||
|
||||
return new VideoResponse
|
||||
{
|
||||
VideoId = video.Id,
|
||||
ChannelId = snippet.ChannelId,
|
||||
Title = snippet.Title,
|
||||
Description = snippet.Description,
|
||||
ThumbnailUrl = snippet.Thumbnails.Medium?.Url ?? snippet.Thumbnails.Default__.Url,
|
||||
PublishedAt = snippet.PublishedAt.GetValueOrDefault(),
|
||||
ViewCount = statistics.ViewCount.GetValueOrDefault(),
|
||||
LikeCount = statistics.LikeCount.GetValueOrDefault(),
|
||||
DislikeCount = statistics.DislikeCount.GetValueOrDefault(),
|
||||
CommentCount = statistics.CommentCount.GetValueOrDefault()
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<VideoResponse>.Failure(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Busca vídeos de um canal na API do YouTube
|
||||
/// </summary>
|
||||
public async Task<Result<List<VideoResponse>>> GetChannelVideosAsync(string channelId, int maxResults = 50)
|
||||
{
|
||||
try
|
||||
{
|
||||
var youtubeService = new YouTubeBaseServiceWrapper(_apiKey);
|
||||
|
||||
var searchRequest = youtubeService.Search.List("snippet");
|
||||
searchRequest.ChannelId = channelId;
|
||||
searchRequest.Type = "video";
|
||||
searchRequest.Order = Google.Apis.YouTube.v3.SearchResource.ListRequest.OrderEnum.Date;
|
||||
searchRequest.MaxResults = maxResults;
|
||||
|
||||
var searchResponse = await searchRequest.ExecuteAsync();
|
||||
var videos = new List<VideoResponse>();
|
||||
|
||||
foreach (var item in searchResponse.Items)
|
||||
{
|
||||
// Para cada resultado, obter os detalhes completos do vídeo
|
||||
var videoResult = await GetVideoDetailsAsync(item.Id.VideoId);
|
||||
if (videoResult.IsSuccess)
|
||||
{
|
||||
videos.Add(videoResult.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return videos;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<List<VideoResponse>>.Failure(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Busca vídeos pelo título ou descrição na API do YouTube
|
||||
/// </summary>
|
||||
public async Task<Result<List<VideoResponse>>> SearchVideosAsync(string query, int maxResults = 10)
|
||||
{
|
||||
try
|
||||
{
|
||||
var youtubeService = new YouTubeBaseServiceWrapper(_apiKey);
|
||||
|
||||
var searchRequest = youtubeService.Search.List("snippet");
|
||||
searchRequest.Q = query;
|
||||
searchRequest.Type = "video";
|
||||
searchRequest.MaxResults = maxResults;
|
||||
|
||||
var searchResponse = await searchRequest.ExecuteAsync();
|
||||
var videos = new List<VideoResponse>();
|
||||
|
||||
foreach (var item in searchResponse.Items)
|
||||
{
|
||||
// Para cada resultado, obter os detalhes completos do vídeo
|
||||
var videoResult = await GetVideoDetailsAsync(item.Id.VideoId);
|
||||
if (videoResult.IsSuccess)
|
||||
{
|
||||
videos.Add(videoResult.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return videos;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result<List<VideoResponse>>.Failure(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -8,7 +8,7 @@ using System.Security.Claims;
|
||||
|
||||
namespace Postall.Infra.Services
|
||||
{
|
||||
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;
|
||||
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Postall/Models/ChannelVideosViewModel.cs
Normal file
11
Postall/Models/ChannelVideosViewModel.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using Postall.Domain.Dtos;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Postall.Models
|
||||
{
|
||||
public class ChannelVideosViewModel
|
||||
{
|
||||
public ChannelResponse Channel { get; set; }
|
||||
public List<VideoViewModel> Videos { get; set; } = new List<VideoViewModel>();
|
||||
}
|
||||
}
|
||||
@ -2,20 +2,19 @@
|
||||
|
||||
namespace Postall.Models
|
||||
{
|
||||
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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">×</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">×</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 + '">');
|
||||
|
||||
136
Postall/Views/Videos/_ChannelVideos.cshtml
Normal file
136
Postall/Views/Videos/_ChannelVideos.cshtml
Normal file
@ -0,0 +1,136 @@
|
||||
@model List<Postall.Models.VideoViewModel>
|
||||
|
||||
@if (Model == null || !Model.Any())
|
||||
{
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i> Não há vídeos disponíveis para este canal.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="form-group mb-3">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
||||
</div>
|
||||
<input type="text" id="videoSearchInput" class="form-control" placeholder="Filtrar vídeos...">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Estilos para os cards de vídeo */
|
||||
.video-item .card {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.video-item .video-image {
|
||||
width: 120px;
|
||||
height: 67px;
|
||||
object-fit: cover;
|
||||
}
|
||||
/* Estilo para o título do vídeo */
|
||||
.video-title {
|
||||
display: block;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* Estilo para a descrição do vídeo */
|
||||
.video-description {
|
||||
height: 40px;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
/* Estilo para o botão ler mais */
|
||||
.read-more-btn {
|
||||
font-size: 0.8rem;
|
||||
color: #007bff;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.read-more-btn:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
/* Estilo para o conteúdo expandido */
|
||||
.description-expanded {
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 0 0 4px 4px;
|
||||
border-top: 1px solid #dee2e6;
|
||||
margin: 10px -20px -20px -20px; /* Margem negativa para alinhar com os limites do card */
|
||||
display: none;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
/* Ajuste na posição do botão Fechar */
|
||||
.card-body {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="row" id="videosList">
|
||||
@foreach (var video in Model)
|
||||
{
|
||||
<div class="col-md-6 mb-3 video-item">
|
||||
<div class="card">
|
||||
<div class="card-body p-3">
|
||||
<div class="d-flex">
|
||||
<div class="flex-shrink-0 mr-3">
|
||||
<img src="@video.ThumbnailUrl" alt="@video.Title" class="img-thumbnail video-image">
|
||||
</div>
|
||||
<div class="flex-grow-1 overflow-hidden">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input video-checkbox" type="checkbox" value="@video.VideoId" id="video-@video.VideoId">
|
||||
<label class="form-check-label w-100" for="video-@video.VideoId">
|
||||
<h6 class="mb-1 video-title">@video.Title</h6>
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-muted small mb-1">Publicado em: @video.PublishedAt.ToString("dd/MM/yyyy")</p>
|
||||
<div>
|
||||
<p class="small video-description mb-0">@video.Description</p>
|
||||
<span class="read-more-btn" data-target="desc-@video.VideoId">Ler mais</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="desc-@video.VideoId" class="description-expanded">
|
||||
<p><b>@video.Title</b></p>
|
||||
@video.Description
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
// Manipular clique no botão "Ler mais"
|
||||
$('.read-more-btn').click(function() {
|
||||
var targetId = $(this).data('target');
|
||||
$('#' + targetId).slideToggle(200);
|
||||
|
||||
// Alternar texto do botão
|
||||
var btnText = $(this).text() === 'Ler mais' ? 'Fechar' : 'Ler mais';
|
||||
$(this).text(btnText);
|
||||
});
|
||||
|
||||
// Filtro de vídeos
|
||||
$('#videoSearchInput').on('keyup', function() {
|
||||
var value = $(this).val().toLowerCase();
|
||||
$('.video-item').filter(function() {
|
||||
var title = $(this).find('.video-title').text().toLowerCase();
|
||||
$(this).toggle(title.indexOf(value) > -1);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
@model List<Postall.Models.VideoViewModel>
|
||||
|
||||
<form id="channelVideosForm">
|
||||
<h6 class="mb-3">Últimos 10 vídeos disponíveis (ordenados por data):</h6>
|
||||
|
||||
@if (Model != null && Model.Any())
|
||||
{
|
||||
<div class="list-group">
|
||||
@foreach (var video in Model)
|
||||
{
|
||||
<div class="list-group-item list-group-item-action flex-column align-items-start">
|
||||
<div class="d-flex">
|
||||
<div class="checkbox-container mr-3 mt-1">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input video-checkbox" type="checkbox" value="@video.Id" id="video-@video.Id">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row w-100">
|
||||
<div class="col-md-4">
|
||||
<img src="@video.ThumbnailUrl" alt="@video.Title" class="img-fluid rounded">
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h5 class="mb-1">@video.Title</h5>
|
||||
<small>@video.PublishedAt.ToString("dd/MM/yyyy")</small>
|
||||
</div>
|
||||
<p class="mb-1">@video.Description</p>
|
||||
<small class="text-muted">
|
||||
<i class="bi bi-youtube"></i>
|
||||
@(video.ChannelTitle ?? "Seu Canal")
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="alert alert-info">
|
||||
Nenhum vídeo encontrado nos canais aos quais você tem acesso.
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
|
||||
<style>
|
||||
.checkbox-container {
|
||||
min-width: 30px;
|
||||
}
|
||||
</style>
|
||||
@ -28,6 +28,7 @@
|
||||
"MongoDbSettings": {
|
||||
"ConnectionString": "mongodb://localhost:27017",
|
||||
"DatabaseName": "YouTubeChannelsDB",
|
||||
"ChannelsCollectionName": "Channels"
|
||||
"ChannelsCollectionName": "Channels",
|
||||
"VideosCollectionName": "Videos"
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user