A Blazor Server application for managing your iTunes movie wishlist with price monitoring. Track HD and 4K price history, set watch prices and receive email notifications when prices drop, all from a local IIS-hosted web app.
| Movie List | Add Movie | Movie Detail |
|---|---|---|
- 📋 Browse all tracked iTunes movies in a sortable, filterable data grid
- 📈 Price history per movie with trend indicators (up / down / all-time low)
- 🎯 Set a personal watch price per movie
- 🔄 Scheduled price checks via Windows Task Scheduler
- 🖱️ Manual price update trigger from the UI
- 🌙 Dark / light mode with system theme detection and localStorage persistence
- 🔍 Filter by title and director
- .NET 10 SDK
- SQL Server (local or remote)
- IIS with ASP.NET Core Hosting Bundle installed
- ASP.NET Core Hosting Bundle for .NET 10
git clone https://github.com/your-username/ItunesMoviePriceTracker.git
cd ItunesMoviePriceTrackerUpdate the connection string in both appsettings.json files (see Configuration below).
dotnet run --project src/ItunesMoviePriceTracker.WebOr open ItunesMoviePriceTracker.sln in Visual Studio and press F5.
The database and all migrations are applied automatically on startup. If the database does not exist it will be created.
To run price checks on a schedule, register the UpdateService with Windows Task Scheduler:
dotnet publish src/ItunesMoviePriceTracker.UpdateService -c Release -o publish/UpdateServiceThen create a new task in Windows Task Scheduler pointing to:
publish/UpdateService/ItunesMoviePriceTracker.UpdateService.exe
Configure the trigger to run at your preferred times. Price checks can also be triggered manually from the Web UI via the Refresh button.
Both ItunesMoviePriceTracker.Web and ItunesMoviePriceTracker.UpdateService require a local appsettings.json that is not committed to the repository. Use appsettings.example.json as a template — copy it and rename it to appsettings.json:
{
"ConnectionStrings": {
"DefaultConnection": "Server=YOUR_SERVER;Database=ItunesMovies;Trusted_Connection=True;TrustServerCertificate=True;"
},
"PriceCheck": {
"ThrottleHours": 24,
"StoreCountry": "se"
},
"UiPolling": {
"IntervalMinutes": 30
},
"DataProtection": {
"KeyPath": "YOUR_KEY_PATH"
},
"Notifications": {
"SmtpHost": "smtp.gmail.com",
"SmtpPort": 587,
"SmtpUser": "YOUR_EMAIL",
"SmtpPassword": "YOUR_APP_PASSWORD",
"ToEmail": "YOUR_EMAIL"
},
"AllowedHosts": "*"
}Replace YOUR_SERVER with your SQL Server instance name, e.g. localhost or .\SQLEXPRESS.
ThrottleHours controls how many hours must pass before a movie is eligible for a new price check. Defaults to 24 if not set.
StoreCountry sets the iTunes store country code. Defaults to se (Sweden) if not set.
UiPolling:IntervalMinutes sets how often the UI polls the database for updates in the background. Defaults to 30 minutes if not set.
DataProtection:KeyPath sets the path where ASP.NET Core Data Protection keys are persisted. Example: C:\\inetpub\\ItunesMoviePriceTracker\\keys. The IIS app pool identity needs write access to this folder.
Notifications — SMTP settings for email notifications when a price drops below WatchPrice.
Download and install from dot.net/download then run:
iisresetmkdir C:\inetpub\ItunesMoviePriceTracker
mkdir C:\inetpub\ItunesMoviePriceTracker\logs
mkdir C:\inetpub\ItunesMoviePriceTracker\keysicacls "C:\inetpub\ItunesMoviePriceTracker" /grant "IIS_IUSRS:(OI)(CI)RX"
icacls "C:\inetpub\ItunesMoviePriceTracker\logs" /grant "IIS_IUSRS:(OI)(CI)F"
icacls "C:\inetpub\ItunesMoviePriceTracker\keys" /grant "IIS_IUSRS:(OI)(CI)F"
icacls "C:\inetpub\ItunesMoviePriceTracker\keys" /grant "IIS APPPOOL\ItunesMoviePriceTracker:(OI)(CI)F"Note: The last command uses the exact application pool identity. Replace
ItunesMoviePriceTrackerwith your site name if different.
Run in SSMS:
USE master;
CREATE LOGIN [IIS APPPOOL\ItunesMoviePriceTracker] FROM WINDOWS;
USE ItunesMovies;
CREATE USER [IIS APPPOOL\ItunesMoviePriceTracker] FOR LOGIN [IIS APPPOOL\ItunesMoviePriceTracker];
ALTER ROLE db_datareader ADD MEMBER [IIS APPPOOL\ItunesMoviePriceTracker];
ALTER ROLE db_datawriter ADD MEMBER [IIS APPPOOL\ItunesMoviePriceTracker];
ALTER ROLE db_ddladmin ADD MEMBER [IIS APPPOOL\ItunesMoviePriceTracker];In IIS Manager → Sites → Add Website:
- Site name:
ItunesMoviePriceTracker - Physical path:
C:\inetpub\ItunesMoviePriceTracker - Port:
8080
In IIS Manager → Application Pools → ItunesMoviePriceTracker → Advanced Settings:
- .NET CLR Version:
No Managed Code - Enable 32-Bit Applications:
False
Run as administrator:
iisreset /stop
dotnet publish src/ItunesMoviePriceTracker.Web -c Release -o C:\inetpub\ItunesMoviePriceTracker
iisreset /startItunesMoviePriceTracker/
├── ItunesMoviePriceTracker.sln
└── src/
├── ItunesMoviePriceTracker.Shared # DTOs only
├── ItunesMoviePriceTracker.Repository # EF Core, MSSQL, internal entities
├── ItunesMoviePriceTracker.Services # Business logic, iTunes API, DI registration
├── ItunesMoviePriceTracker.Web # Blazor Server (.NET 10), IIS hosted
└── ItunesMoviePriceTracker.UpdateService # Console App, Windows Task Scheduler
Shared ← Services ← Web
← UpdateService
Repository ← Services
Sharedhas zero dependencies — DTOs onlyRepositoryhas zero dependencies on other projects — only EF Core NuGet packagesServicesreferencesSharedandRepository, contains service interfaces, implementations and DI registration via extension methodsWebandUpdateServicereferenceSharedandServicesonly —Repositoryis wired up via DI inProgram.cs
Web → (DTOs in Shared) → Service → Repository → MSSQL
↑
EF entity mapped to DTO in Service layer
| Convention | Choice |
|---|---|
| Constructors | Primary constructors (C# 12) |
| Async | Async/await throughout |
| Nullable | Nullable reference types enabled |
| Naming | PascalCase for classes/methods, camelCase for locals |
Example:
// ✅ Primary constructor
public class MovieRepository(AppDbContext context, int throttleHours) : IMovieRepository
{
public async Task<IEnumerable<Movie>> GetAllAsync()
=> await context.Movies.Include(m => m.Prices).OrderBy(m => m.TrackHdPrice).ToListAsync();
}
// ❌ Old style
public class MovieRepository : IMovieRepository
{
private readonly AppDbContext _context;
public MovieRepository(AppDbContext context) { _context = context; }
}| Area | Choice | Reason |
|---|---|---|
| Framework | .NET 10 / Blazor Server | Modern, interactive server rendering |
| Database | MSSQL (existing) | Preserves years of accumulated price data |
| ORM | EF Core Code First | Migrations handle schema changes cleanly |
| Hosting | IIS (local) | Consistent with existing setup |
| Price updates | Windows Task Scheduler + manual UI trigger | Simple, reliable, no external dependencies |
| iTunes data | iTunes Search API (Apple) | Proven, no scraping needed |
| HTTP | IHttpClientFactory | Best practice for HttpClient lifetime management |
| Theme | System detection + localStorage | Respects user preference, persists across navigation |
| Logging | Serilog with file sink | Daily rolling log files, EF Core noise filtered out |
Sharedexposes only DTOs — no interfaces, no EF entities leak into WebRepositoryis fully internal — EF entities never leave the Repository layerServicesowns all mapping between EF entities and DTOsServicesowns DI registration via extension methods —WebandUpdateServicecallbuilder.Services.AddMovieServices()inProgram.cswithout knowing anything aboutRepository
Trend is intentionally calculated in the UI layer (Blazor component), not in services. It is a display concern — no enum or helper needed. The Trend property is set on the DTO after data is loaded.
- Store country configurable via
PriceCheck:StoreCountryinappsettings.json(defaultse) - 3-second delay between API calls — respects Apple rate limits
- Movies only checked if
LastCheckedis older thanThrottleHours(configurable, default 24h)
Serilog is used for structured logging with a daily rolling file sink. EF Core database command logging is suppressed at Warning level to avoid noise. Log files are stored in the logs/ folder of each deployed application and rotated daily with a 30-day retention policy.
On startup, ApplyMigrationsAsync() is called via Program.cs. This creates the database if it does not exist and applies any pending migrations automatically. No manual dotnet ef database update needed.
Theme is managed entirely in JavaScript — Blazor never touches it. On page load, the saved localStorage value is applied. If no preference is saved, the system theme is used. The toggle button in the header persists the choice to localStorage.
dbo.Movies
| Column | Type | Notes |
|---|---|---|
| TrackId | int PK | iTunes Track ID |
| TrackName | nvarchar(500) | Movie title |
| ReleaseDate | datetime2(7) | |
| ArtistName | nvarchar(255) | Director |
| LongDescription | nvarchar(2000) | |
| ArtworkUrl60 | nvarchar(500) | Thumbnail |
| ArtworkUrl400 | nvarchar(500) | Full artwork |
| TrackHdPrice | decimal(18,2) | Latest known price |
| LastChecked | datetime2(7) | Throttle control |
| WatchPrice | decimal(18,2) | Target watch price (nullable) |
dbo.Prices
| Column | Type | Notes |
|---|---|---|
| Id | int PK | |
| Date | datetime2(7) | UTC |
| Price | decimal(18,2) | |
| MovieTrackId | int FK | → dbo.Movies.TrackId |
- Multi-language support (UI layer, resource strings)
- Support for additional iTunes store countries
- Desktop app version using Blazor + WebView2 — wraps the existing app in a Windows
.exeinstaller, eliminating the need for IIS and SQL Server setup for end users
This project is licensed under the MIT License.