Compare commits

..

2 Commits

Author SHA1 Message Date
5f8fb01a9f move project 2025-07-27 00:33:05 +02:00
4544be2999 remove adapter for more ease of development 2025-07-26 12:39:22 +02:00
101 changed files with 983 additions and 120 deletions

15
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "C#: <project-name> Debug",
"type": "dotnet",
"request": "launch",
"projectPath": "${workspaceFolder}/<relative-path-to-project-folder><project-name>.csproj"
}
]
}

89
AdapterContext.cs Normal file
View File

@@ -0,0 +1,89 @@
using Db.Models;
using Microsoft.EntityFrameworkCore;
public class AdapterContext : DbContext
{
public AdapterContext(DbContextOptions<AdapterContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Configure your entity mappings here
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<VideoTag>(e =>
{
e.HasKey(vt => new { vt.VideoId, vt.TagId });
e.HasOne(vt => vt.Video)
.WithMany(v => v.VideoTags)
.HasForeignKey(vt => vt.VideoId);
e.HasOne(vt => vt.Tag)
.WithMany(t => t.VideoTags)
.HasForeignKey(vt => vt.TagId);
});
modelBuilder.Entity<Video>(e =>
{
e.HasKey(v => v.Id);
e.Property(v => v.Id).IsRequired();
e.Property(v => v.Extension).IsRequired();
// delete videoTags when a video is deleted but not the tags
e.HasMany(v => v.VideoTags)
.WithOne(vt => vt.Video)
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity<Tag>(e =>
{
e.HasKey(t => t.Name);
e.Property(t => t.Name).IsRequired();
// delete videoTags when a tag is deleted but not the videos
e.HasMany(t => t.VideoTags)
.WithOne(vt => vt.Tag)
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity<Setting>(e =>
{
e.HasKey(s => s.Name);
e.Property(s => s.Name).IsRequired();
e.Property(s => s.Value).IsRequired();
});
}
// Tags a video can have (e.g. "funny", "cat", "dog")
public DbSet<Tag> Tags { get; set; }
// Videos with tags, relative paths, and other metadata
public DbSet<Video> Videos { get; set; }
// Settings for the adapter, such as video path
public DbSet<Setting> Settings { get; set; }
public DbSet<VideoTag> VideoTags { get; set; }
public async Task<T?> GetSettingAsync<T>(SettingName settingName)
{
var setting = await Settings.FindAsync(settingName);
return setting != null ? (T?)Convert.ChangeType(setting.Value, typeof(T)) : default;
}
public async Task SetSettingAsync<T>(SettingName settingName, T value)
{
var setting = await Settings.FindAsync(settingName);
if (setting == null)
{
setting = new Setting { Name = settingName, Value = value?.ToString() ?? string.Empty };
Settings.Add(setting);
}
else
{
setting.Value = value?.ToString() ?? string.Empty;
Settings.Update(setting);
}
await SaveChangesAsync();
}
}

View File

@@ -0,0 +1,241 @@
using System.Diagnostics;
using System.Threading.Tasks;
using Db.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using TagVid.Helpers;
using TagVid.Models;
namespace TagVid.Controllers;
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly AdapterContext _adapterContext;
public HomeController(ILogger<HomeController> logger, AdapterContext adapterContext)
{
_logger = logger;
_adapterContext = adapterContext;
}
public async Task<IActionResult> Index()
{
// check if the video path was set
var pathSettings = await _adapterContext.GetSettingAsync<string>(SettingName.VideoPath);
if (string.IsNullOrEmpty(pathSettings))
{
_logger.LogWarning("Video path is not set. Please configure the video path in settings.");
return RedirectToAction("Settings", "Home");
}
return View();
}
[HttpGet]
[Route("Home/Index/{id}")]
public async Task<IActionResult> Index(string id)
{
throw new NotImplementedException();
}
public async Task<IActionResult> Settings()
{
var vm = new SettingsViewModel
{
VideoPath = await _adapterContext.GetSettingAsync<string>(SettingName.VideoPath)
};
return View(vm);
}
[HttpPost]
public async Task<IActionResult> Settings(SettingsViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
// Validate the video path
if (string.IsNullOrWhiteSpace(model.VideoPath))
{
ModelState.AddModelError(nameof(model.VideoPath), "Video path cannot be empty.");
return View(model);
}
// Check if the path exists
if (!Directory.Exists(model.VideoPath))
{
ModelState.AddModelError(nameof(model.VideoPath), "The specified video path does not exist.");
return View(model);
}
// Check if the path is a directory
if (!System.IO.File.GetAttributes(model.VideoPath).HasFlag(FileAttributes.Directory))
{
ModelState.AddModelError(nameof(model.VideoPath), "The specified path must be a directory.");
return View(model);
}
// Save settings to the database
await _adapterContext.SetSettingAsync(SettingName.VideoPath, model.VideoPath);
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Upload(IFormFile videoFile)
{
_logger.LogInformation("Upload called with file: {FileName}", videoFile?.Name);
if (videoFile == null || videoFile.Length == 0)
{
return RedirectToAction("Index");
}
// Validate the video file
// no size limit for now, but could be added later
if (!videoFile.ContentType.StartsWith("video/"))
{
return RedirectToAction("Index");
}
// Save the video file to the configured path
var videoPath = await _adapterContext.GetSettingAsync<string>(SettingName.VideoPath);
var ext = Path.GetExtension(videoFile.FileName);
if (string.IsNullOrEmpty(videoPath) || !Directory.Exists(videoPath))
{
return RedirectToAction("Settings");
}
// asp.net docu suggests not using .FileName due to security reasons
var id = Guid.NewGuid().ToString();
var filePath = Path.Combine(videoPath, $"{id}{ext}");
using (var stream = new FileStream(filePath, FileMode.Create))
{
await videoFile.CopyToAsync(stream);
}
var data = await new FFProbeHelper<HomeController>(_logger).GetVideoMetadataAsync(filePath);
var video = new Video
{
Id = id,
Duration = data?.Duration ?? TimeSpan.Zero,
Width = data?.Width ?? 0,
Height = data?.Height ?? 0,
Extension = ext,
VideoTags = []
};
_adapterContext.Videos.Add(video);
await _adapterContext.SaveChangesAsync();
// redirect to the video details page
return RedirectToAction("Details", "Home", new { id = video.Id });
}
public async Task<IActionResult> Details(string id)
{
if (string.IsNullOrEmpty(id))
{
return NotFound();
}
var video = await _adapterContext.Videos
.Include(v => v.VideoTags)
.ThenInclude(vt => vt.Tag)
.FirstOrDefaultAsync(v => v.Id == id);
if (video == null)
{
return NotFound();
}
var vm = new DetailsViewModel
{
VideoId = id,
Title = video?.Title ?? string.Empty,
Tags = video?.VideoTags.Select(vt => vt.Tag.Name).ToList() ?? []
};
return View(vm);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Details(DetailsViewModel videoModel)
{
if (string.IsNullOrEmpty(videoModel.VideoId))
{
return NotFound();
}
var existingVideo = await _adapterContext.Videos
.Include(v => v.VideoTags)
.ThenInclude(vt => vt.Tag)
.FirstOrDefaultAsync(v => v.Id == videoModel.VideoId);
if (existingVideo == null)
{
return NotFound();
}
// Update video title
existingVideo.Title = videoModel.Title;
// Update tags
existingVideo.VideoTags.Clear();
foreach (var tagName in videoModel.Tags)
{
var tag = await _adapterContext.Tags.FirstOrDefaultAsync(t => t.Name == tagName);
if (tag == null)
{
tag = new Tag { Name = tagName };
_adapterContext.Tags.Add(tag);
}
existingVideo.VideoTags.Add(new VideoTag { Video = existingVideo, Tag = tag });
}
_adapterContext.Videos.Update(existingVideo);
await _adapterContext.SaveChangesAsync();
return RedirectToAction("Details", new { id = videoModel.VideoId });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(string id)
{
if (string.IsNullOrEmpty(id))
{
return NotFound();
}
var video = await _adapterContext.Videos.FindAsync(id);
if (video == null)
{
return NotFound();
}
// Delete the video file from the file system
var videoPath = await _adapterContext.GetSettingAsync<string>(SettingName.VideoPath);
if (!string.IsNullOrEmpty(videoPath))
{
var filePath = Path.Combine(videoPath, $"{video.Id}{video.Extension}");
if (System.IO.File.Exists(filePath))
{
System.IO.File.Delete(filePath);
}
}
// Remove the video from the database
_adapterContext.Videos.Remove(video);
await _adapterContext.SaveChangesAsync();
return RedirectToAction("Index");
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}

View File

@@ -0,0 +1,123 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace TagVid.Migrations
{
[DbContext(typeof(AdapterContext))]
[Migration("20250726215357_InitialMigration")]
partial class InitialMigration
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Db.Models.Setting", b =>
{
b.Property<int>("Name")
.HasColumnType("int");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Name");
b.ToTable("Settings");
});
modelBuilder.Entity("Db.Models.Tag", b =>
{
b.Property<string>("Name")
.HasColumnType("nvarchar(450)");
b.HasKey("Name");
b.ToTable("Tags");
});
modelBuilder.Entity("Db.Models.Video", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<TimeSpan>("Duration")
.HasColumnType("time");
b.Property<string>("Extension")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("Height")
.HasColumnType("int");
b.Property<string>("Title")
.HasColumnType("nvarchar(max)");
b.Property<int>("Width")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("Videos");
});
modelBuilder.Entity("Db.Models.VideoTag", b =>
{
b.Property<string>("VideoId")
.HasColumnType("nvarchar(450)");
b.Property<string>("TagId")
.HasColumnType("nvarchar(450)");
b.HasKey("VideoId", "TagId");
b.HasIndex("TagId");
b.ToTable("VideoTags");
});
modelBuilder.Entity("Db.Models.VideoTag", b =>
{
b.HasOne("Db.Models.Tag", "Tag")
.WithMany("VideoTags")
.HasForeignKey("TagId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Db.Models.Video", "Video")
.WithMany("VideoTags")
.HasForeignKey("VideoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Tag");
b.Navigation("Video");
});
modelBuilder.Entity("Db.Models.Tag", b =>
{
b.Navigation("VideoTags");
});
modelBuilder.Entity("Db.Models.Video", b =>
{
b.Navigation("VideoTags");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,99 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace TagVid.Migrations
{
/// <inheritdoc />
public partial class InitialMigration : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Settings",
columns: table => new
{
Name = table.Column<int>(type: "int", nullable: false),
Value = table.Column<string>(type: "nvarchar(max)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Settings", x => x.Name);
});
migrationBuilder.CreateTable(
name: "Tags",
columns: table => new
{
Name = table.Column<string>(type: "nvarchar(450)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Tags", x => x.Name);
});
migrationBuilder.CreateTable(
name: "Videos",
columns: table => new
{
Id = table.Column<string>(type: "nvarchar(450)", nullable: false),
Title = table.Column<string>(type: "nvarchar(max)", nullable: true),
Duration = table.Column<TimeSpan>(type: "time", nullable: false),
Width = table.Column<int>(type: "int", nullable: false),
Height = table.Column<int>(type: "int", nullable: false),
Extension = table.Column<string>(type: "nvarchar(max)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Videos", x => x.Id);
});
migrationBuilder.CreateTable(
name: "VideoTags",
columns: table => new
{
VideoId = table.Column<string>(type: "nvarchar(450)", nullable: false),
TagId = table.Column<string>(type: "nvarchar(450)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_VideoTags", x => new { x.VideoId, x.TagId });
table.ForeignKey(
name: "FK_VideoTags_Tags_TagId",
column: x => x.TagId,
principalTable: "Tags",
principalColumn: "Name",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_VideoTags_Videos_VideoId",
column: x => x.VideoId,
principalTable: "Videos",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_VideoTags_TagId",
table: "VideoTags",
column: "TagId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Settings");
migrationBuilder.DropTable(
name: "VideoTags");
migrationBuilder.DropTable(
name: "Tags");
migrationBuilder.DropTable(
name: "Videos");
}
}
}

View File

@@ -0,0 +1,120 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace TagVid.Migrations
{
[DbContext(typeof(AdapterContext))]
partial class AdapterContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Db.Models.Setting", b =>
{
b.Property<int>("Name")
.HasColumnType("int");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Name");
b.ToTable("Settings");
});
modelBuilder.Entity("Db.Models.Tag", b =>
{
b.Property<string>("Name")
.HasColumnType("nvarchar(450)");
b.HasKey("Name");
b.ToTable("Tags");
});
modelBuilder.Entity("Db.Models.Video", b =>
{
b.Property<string>("Id")
.HasColumnType("nvarchar(450)");
b.Property<TimeSpan>("Duration")
.HasColumnType("time");
b.Property<string>("Extension")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("Height")
.HasColumnType("int");
b.Property<string>("Title")
.HasColumnType("nvarchar(max)");
b.Property<int>("Width")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("Videos");
});
modelBuilder.Entity("Db.Models.VideoTag", b =>
{
b.Property<string>("VideoId")
.HasColumnType("nvarchar(450)");
b.Property<string>("TagId")
.HasColumnType("nvarchar(450)");
b.HasKey("VideoId", "TagId");
b.HasIndex("TagId");
b.ToTable("VideoTags");
});
modelBuilder.Entity("Db.Models.VideoTag", b =>
{
b.HasOne("Db.Models.Tag", "Tag")
.WithMany("VideoTags")
.HasForeignKey("TagId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Db.Models.Video", "Video")
.WithMany("VideoTags")
.HasForeignKey("VideoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Tag");
b.Navigation("Video");
});
modelBuilder.Entity("Db.Models.Tag", b =>
{
b.Navigation("VideoTags");
});
modelBuilder.Entity("Db.Models.Video", b =>
{
b.Navigation("VideoTags");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,8 @@
namespace TagVid.Models;
public class DetailsViewModel
{
public string VideoId { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public List<string> Tags { get; set; } = new List<string>();
}

View File

@@ -1,4 +1,4 @@
namespace tagvid.Models;
namespace TagVid.Models;
public class ErrorViewModel
{

View File

@@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;
namespace TagVid.Models;
public class SettingsViewModel
{
[Required(ErrorMessage = "Video path is required.")]
[Display(Name = "Video Path")]
public string? VideoPath { get; set; } = null;
public string? ErrorMessage { get; set; }
}

View File

@@ -1,8 +1,26 @@
using Microsoft.AspNetCore.Http.Features;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
// increase the default upload size limit
builder.Services.Configure<FormOptions>(options =>
{
options.MultipartBodyLengthLimit = long.MaxValue; // Set to a very high value
});
builder.WebHost.ConfigureKestrel(serverOptions =>
{
serverOptions.Limits.MaxRequestBodySize = long.MaxValue; // Set to a very high value
});
// Configure Entity Framework Core with SQL Server
builder.Services.AddDbContext<AdapterContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
var app = builder.Build();
// Configure the HTTP request pipeline.

8
SettingName.cs Normal file
View File

@@ -0,0 +1,8 @@
public enum SettingName
{
/// <summary>
/// Represents a the absolute path of the folder where videos are stored.
/// </summary>
VideoPath,
}

70
Views/Home/Details.cshtml Normal file
View File

@@ -0,0 +1,70 @@
@model DetailsViewModel
@using (Html.BeginForm("Details", "Home", FormMethod.Post )) {
@Html.AntiForgeryToken()
@Html.HiddenFor(m => m.VideoId)
<div class="mb-3">
@Html.LabelFor(m => m.Title)
@Html.TextBoxFor(m => m.Title, new { @class = "form-control", @required = "required" })
@Html.ValidationMessageFor(m => m.Title)
</div>
<div class="mb-3">
@Html.LabelFor(m => m.Tags)
@foreach (var tag in Model.Tags)
{
<span class="badge rounded-pill text-bg-secondary" data-tag-name="@tag">
@Html.DisplayFor(m => tag)
<button type="button" class="btn-close btn-sm" aria-label="Close" onclick="removeTag('@tag')"></button>
</span>
}
<input type="text" id="newTag" class="form-control mt-2" placeholder="Add new tag" />
@for (int i = 0; i < Model.Tags.Count; i++)
{
<input type="hidden" class="hiddenTag" name="Tags[@i]" value="@Model.Tags.ElementAt(i)" />
}
</div>
<button type="submit" class="btn btn-primary">Update Video</button>
}
@section Scripts {
<script>
$(document).ready(function() {
$('#newTag').on('keypress', function(e) {
if (e.which === 13) { // Enter key
e.preventDefault();
var tagName = $(this).val().trim();
if (tagName) {
$(this).val(''); // Clear input
// check if a hidden input for this tag already exists
var existingTag = $(`input[value='${tagName}']`);
if (existingTag.length > 0) {
return;
}
var tagCount = $('.hiddenTag').length;
// Add the tag to a new hidden input
var hiddenInput = `<input type="hidden" class="hiddenTag" name="Tags[${tagCount}]" value="${tagName}" />`;
var tagHtml = `<span class="badge rounded-pill text-bg-secondary" data-tag-name="${tagName}">
${tagName}
<button type="button" class="btn-close btn-sm" aria-label="Close" onclick="removeTag('${tagName}')"></button>
</span>`;
$(this).after(hiddenInput);
$(this).before(tagHtml);
}
}
});
window.removeTag = function(tagName) {
// Remove the hidden input for the tag
$(`input[value='${tagName}']`).remove();
// Remove the badge from the UI
$(`span[data-tag-name='${tagName}']`).remove();
};
});
</script>
}

1
Views/Home/Index.cshtml Normal file
View File

@@ -0,0 +1 @@


View File

@@ -0,0 +1,15 @@
@model SettingsViewModel
@using (Html.BeginForm("Settings", "Home", FormMethod.Post)) {
@Html.AntiForgeryToken()
<h2>Settings</h2>
<div class="mb-3">
@Html.LabelFor(m => m.VideoPath, new { @class = "form-label" })
@Html.TextBoxFor(m => m.VideoPath, new { @class = "form-control" })
@Html.ValidationMessageFor(m => m.VideoPath, "", new { @class = "text-danger" })
</div>
<button type="submit" class="btn btn-primary">Save Settings</button>
}

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - tagvid</title>
<title>Tagvid</title>
<script type="importmap"></script>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
@@ -24,9 +24,14 @@
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Settings">Settings</a>
</li>
</ul>
<form class="d-flex" role="upload" method="post" asp-controller="Home" asp-action="Upload" enctype="multipart/form-data">
<input type="file" class="form-control me-2" id="videoFileInput" name="videoFile" required accept="video/*" />
<input type="hidden" name="previousUrl" value="@Context.Request.Path" />
<button type="submit" class="btn btn-outline-success">Upload</button>
</form>
</div>
</div>
</nav>
@@ -37,11 +42,6 @@
</main>
</div>
<footer class="border-top footer text-muted">
<div class="container">
&copy; 2025 - tagvid - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</div>
</footer>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>

View File

@@ -0,0 +1,4 @@
@using TagVid
@using TagVid.Models
@using Db.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@@ -1,6 +0,0 @@
namespace adapter;
public class Class1
{
}

View File

@@ -1,9 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

13
appsettings.json Normal file
View File

@@ -0,0 +1,13 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Information",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Server=192.168.178.36;Database=TagVid;User Id=sa;Password=TngxW9Xp8QxXyb3GMxYz; Integrated Security=False; trustServerCertificate=true; Encrypt=False; MultipleActiveResultSets=True; Connection Timeout=30;"
}
}

13
db/models/Setting.cs Normal file
View File

@@ -0,0 +1,13 @@
namespace Db.Models;
public class Setting
{
public required SettingName Name { get; set; }
public required string Value { get; set; }
// methods to convert Value to specific types that are not connected to entity framework
public T GetValue<T>()
{
return (T)Convert.ChangeType(Value, typeof(T));
}
}

10
db/models/Tag.cs Normal file
View File

@@ -0,0 +1,10 @@
namespace Db.Models;
public class Tag
{
// We use the name of the tag as the Id to ensure uniqueness
public required string Name { get; set; }
public ICollection<VideoTag> VideoTags { get; set; } = [];
}

19
db/models/Video.cs Normal file
View File

@@ -0,0 +1,19 @@
namespace Db.Models;
public class Video
{
// Disable warning as Id is required but not set in the constructor.
// This is to ensure that the Id is set by the database when the entity is added
#pragma warning disable CS8618
public string Id { get; set; }
#pragma warning restore CS8618
public string? Title { get; set; }
public TimeSpan Duration { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public string Extension { get; set; } = string.Empty; // File extension of the video file
public ICollection<VideoTag> VideoTags { get; set; } = [];
}

10
db/models/VideoTag.cs Normal file
View File

@@ -0,0 +1,10 @@
namespace Db.Models;
public class VideoTag
{
public string VideoId { get; set; }
public Video Video { get; set; }
public string TagId { get; set; }
public Tag Tag { get; set; }
}

60
helpers/FFProbeHelper.cs Normal file
View File

@@ -0,0 +1,60 @@
using System.Diagnostics;
using System.Text.Json;
namespace TagVid.Helpers;
public class VideoMetadata
{
public int Width { get; set; }
public int Height { get; set; }
public TimeSpan Duration { get; set; }
}
public class FFProbeHelper<T> where T : class
{
private static ILogger<T> _logger;
public FFProbeHelper(ILogger<T> logger)
{
_logger = logger;
}
public async Task<VideoMetadata?> GetVideoMetadataAsync(string filePath)
{
var startInfo = new ProcessStartInfo
{
FileName = "ffprobe",
Arguments = $"-v quiet -print_format json -show_streams \"{filePath}\"",
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var process = new Process { StartInfo = startInfo };
process.Start();
string output = await process.StandardOutput.ReadToEndAsync();
await process.WaitForExitAsync();
using var jsonDoc = JsonDocument.Parse(output);
var streams = jsonDoc.RootElement.GetProperty("streams");
var videoStream = streams.EnumerateArray()
.FirstOrDefault(s => s.GetProperty("codec_type").GetString() == "video");
if (videoStream.ValueKind == JsonValueKind.Undefined)
return null;
videoStream.TryGetProperty("duration", out var durEl1);
_logger.LogInformation($"{videoStream.GetProperty("width")}x{videoStream.GetProperty("height")}, Duration: {durEl1}");
var metadata = new VideoMetadata
{
Width = videoStream.GetProperty("width").GetInt32(),
Height = videoStream.GetProperty("height").GetInt32(),
Duration = durEl1.ValueKind == JsonValueKind.String
? TimeSpan.FromSeconds(double.Parse(durEl1.GetString() ?? "0"))
: TimeSpan.FromSeconds(durEl1.GetDouble())
};
return metadata;
}
}

19
tagvid.csproj Normal file
View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.7" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>

View File

@@ -1,48 +1,24 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
VisualStudioVersion = 17.5.2.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "tagvid", "tagvid\tagvid.csproj", "{89456151-48D7-47F2-96AE-B27E62947059}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "adapter", "adapter\adapter.csproj", "{505FCE94-EFFC-436D-BC39-315F7D793475}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "tagvid", "tagvid.csproj", "{147974D8-F7CC-E7DF-9998-54200E639E4B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{89456151-48D7-47F2-96AE-B27E62947059}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{89456151-48D7-47F2-96AE-B27E62947059}.Debug|Any CPU.Build.0 = Debug|Any CPU
{89456151-48D7-47F2-96AE-B27E62947059}.Debug|x64.ActiveCfg = Debug|Any CPU
{89456151-48D7-47F2-96AE-B27E62947059}.Debug|x64.Build.0 = Debug|Any CPU
{89456151-48D7-47F2-96AE-B27E62947059}.Debug|x86.ActiveCfg = Debug|Any CPU
{89456151-48D7-47F2-96AE-B27E62947059}.Debug|x86.Build.0 = Debug|Any CPU
{89456151-48D7-47F2-96AE-B27E62947059}.Release|Any CPU.ActiveCfg = Release|Any CPU
{89456151-48D7-47F2-96AE-B27E62947059}.Release|Any CPU.Build.0 = Release|Any CPU
{89456151-48D7-47F2-96AE-B27E62947059}.Release|x64.ActiveCfg = Release|Any CPU
{89456151-48D7-47F2-96AE-B27E62947059}.Release|x64.Build.0 = Release|Any CPU
{89456151-48D7-47F2-96AE-B27E62947059}.Release|x86.ActiveCfg = Release|Any CPU
{89456151-48D7-47F2-96AE-B27E62947059}.Release|x86.Build.0 = Release|Any CPU
{505FCE94-EFFC-436D-BC39-315F7D793475}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{505FCE94-EFFC-436D-BC39-315F7D793475}.Debug|Any CPU.Build.0 = Debug|Any CPU
{505FCE94-EFFC-436D-BC39-315F7D793475}.Debug|x64.ActiveCfg = Debug|Any CPU
{505FCE94-EFFC-436D-BC39-315F7D793475}.Debug|x64.Build.0 = Debug|Any CPU
{505FCE94-EFFC-436D-BC39-315F7D793475}.Debug|x86.ActiveCfg = Debug|Any CPU
{505FCE94-EFFC-436D-BC39-315F7D793475}.Debug|x86.Build.0 = Debug|Any CPU
{505FCE94-EFFC-436D-BC39-315F7D793475}.Release|Any CPU.ActiveCfg = Release|Any CPU
{505FCE94-EFFC-436D-BC39-315F7D793475}.Release|Any CPU.Build.0 = Release|Any CPU
{505FCE94-EFFC-436D-BC39-315F7D793475}.Release|x64.ActiveCfg = Release|Any CPU
{505FCE94-EFFC-436D-BC39-315F7D793475}.Release|x64.Build.0 = Release|Any CPU
{505FCE94-EFFC-436D-BC39-315F7D793475}.Release|x86.ActiveCfg = Release|Any CPU
{505FCE94-EFFC-436D-BC39-315F7D793475}.Release|x86.Build.0 = Release|Any CPU
{147974D8-F7CC-E7DF-9998-54200E639E4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{147974D8-F7CC-E7DF-9998-54200E639E4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{147974D8-F7CC-E7DF-9998-54200E639E4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{147974D8-F7CC-E7DF-9998-54200E639E4B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BB1808E1-0EBB-4D0E-9A3C-0B133B761E43}
EndGlobalSection
EndGlobal

View File

@@ -1,31 +0,0 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using tagvid.Models;
namespace tagvid.Controllers;
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
public HomeController(ILogger<HomeController> logger)
{
_logger = logger;
}
public IActionResult Index()
{
return View();
}
public IActionResult Privacy()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}

View File

@@ -1,8 +0,0 @@
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

View File

@@ -1,3 +0,0 @@
@using tagvid
@using tagvid.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@@ -1,9 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@@ -1,13 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<ProjectReference Include="..\adapter\adapter.csproj" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>

View File

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Some files were not shown because too many files have changed in this diff Show More