diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..a2cdd09 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,69 @@ +name: Docker Build & Publish + +on: + push: + branches: [ "main" ] + +jobs: + build: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 + with: + fetch-depth: 0 + + - name: Install GitVersion + uses: gittools/actions/gitversion/setup@8fedf8a6f6cec22c9eaa66ee693f5b1f315dcbdd + with: + versionSpec: '6.0.5' + preferLatestVersion: true + + - name: Determine version + id: gitversion + uses: gittools/actions/gitversion/execute@8fedf8a6f6cec22c9eaa66ee693f5b1f315dcbdd + with: + useConfigFile: true + configFilePath: GitVersion.yml + + - name: Set up QEMU + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd + + - name: Log into Docker Hub + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 + with: + context: ./src + file: ./src/Dockerfile + push: true + platforms: linux/amd64,linux/arm64 + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/mediabrowser:latest + ${{ secrets.DOCKERHUB_USERNAME }}/mediabrowser:${{ steps.gitversion.outputs.fullSemVer }} + build-args: | + ASSEMBLY_SEM_FILE_VER_ARG=${{ steps.gitversion.outputs.assemblySemFileVer }} + ASSEMBLY_SEM_VER_ARG=${{ steps.gitversion.outputs.assemblySemVer }} + FULL_SEM_VER_ARG=${{ steps.gitversion.outputs.fullSemVer }} + INFORMATIONAL_VERSION_ARG=${{ steps.gitversion.outputs.informationalVersion }} + + - name: Tag repository with version + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag -a "${{ steps.gitversion.outputs.semVer }}" -m "Release ${{ steps.gitversion.outputs.semVer }}" + git push origin "${{ steps.gitversion.outputs.semVer }}" + gh release create "${{ steps.gitversion.outputs.semVer }}" --generate-notes diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml new file mode 100644 index 0000000..9e3e9ef --- /dev/null +++ b/.github/workflows/pull-requests.yml @@ -0,0 +1,80 @@ +name: Pull Request + +on: + pull_request: + branches: [ "main" ] + +jobs: + test: + runs-on: ubuntu-latest + + services: + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: password + MYSQL_DATABASE: mediabrowser + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + + postgres: + image: postgres:latest + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + POSTGRES_DB: mediabrowser + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + sqlserver: + image: mcr.microsoft.com/mssql/server:2022-latest + env: + SA_PASSWORD: "Password123!" + ACCEPT_EULA: "Y" + MSSQL_PID: "Express" + ports: + - 1433:1433 + options: >- + --health-cmd="exit 0" + --health-interval 10s + --health-timeout 5s + --health-retries 10 + + steps: + - name: Checkout repository + uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@2016bd2012dba4e32de620c46fe006a3ac9f0602 + with: + dotnet-version: 10.x + + - name: Restore dependencies + run: dotnet restore src/MediaBrowser.Tests/MediaBrowser.Tests.csproj + + - name: Build + run: dotnet build src/MediaBrowser.Tests/MediaBrowser.Tests.csproj --no-restore + + - name: Install FFmpeg + run: sudo apt-get update && sudo apt-get install -y ffmpeg + + - name: Create index.html for tests + run: | + mkdir -p src/MediaBrowser/wwwroot + echo '
' > src/MediaBrowser/wwwroot/index.html + + - name: Test + run: dotnet test src/MediaBrowser.Tests/MediaBrowser.Tests.csproj --no-build --verbosity normal + env: + LOGGING__LOGLEVEL__DEFAULT: "Error" + DB__MYSQLCONNECTIONSTRING: "Server=127.0.0.1;Port=3306;Database=mediabrowser;User=root;Password=password;" + DB__POSTGRESCONNECTIONSTRING: "Host=127.0.0.1;Port=5432;Database=mediabrowser;Username=postgres;Password=password;" + DB__SQLSERVERCONNECTIONSTRING: "Server=127.0.0.1,1433;Database=mediabrowser;User Id=sa;Password=Password123!;TrustServerCertificate=True;" diff --git a/.gitignore b/.gitignore index 91251b8..9780df1 100644 --- a/.gitignore +++ b/.gitignore @@ -388,5 +388,8 @@ testem.log /typings # System Files +temp/* .DS_Store Thumbs.db +src/.idea/** +*.db diff --git a/GitVersion.yml b/GitVersion.yml new file mode 100644 index 0000000..f6a0a46 --- /dev/null +++ b/GitVersion.yml @@ -0,0 +1,8 @@ +mode: ContinuousDeployment +increment: Patch +next-version: 0.1.0 +commit-message-incrementing: Enabled +branches: {} +ignore: + sha: [] +minor-version-bump-message: '(^feat|\+semver:\s?(feature|minor))' \ No newline at end of file diff --git a/README.md b/README.md index dda1b45..8bf10bc 100644 --- a/README.md +++ b/README.md @@ -1 +1,77 @@ -# MediaBrowser \ No newline at end of file +# MediaBrowser + +# Schema Updates + +After making changes, run these commands from the root of the repo: +```sh +export NAME=InitialCreate +cd src/MediaBrowser + +export db__type=sqlite +dotnet ef migrations add $NAME --project ../Migrations/MediaBrowser.Sqlite/MediaBrowser.Sqlite.csproj +dotnet ef migrations script --output ../Migrations/MediaBrowser.Sqlite/schema.sql + +export db__type=mySql +dotnet ef migrations add $NAME --project ../Migrations/MediaBrowser.MySql/MediaBrowser.MySql.csproj +dotnet ef migrations script --output ../Migrations/MediaBrowser.MySql/schema.sql --idempotent + +export db__type=postgres +dotnet ef migrations add $NAME --project ../Migrations/MediaBrowser.Postgres/MediaBrowser.Postgres.csproj +dotnet ef migrations script --output ../Migrations/MediaBrowser.Postgres/schema.sql --idempotent + +export db__type=sqlServer +dotnet ef migrations add $NAME --project ../Migrations/MediaBrowser.SqlServer/MediaBrowser.SqlServer.csproj +dotnet ef migrations script --output ../Migrations/MediaBrowser.SqlServer/schema.sql --idempotent +``` + +# Docker + +To build the docker image run the following from the root of the repository: +```sh +export FULL_SEM_VER=1.0.0 +cd src +docker build -t mediabrowser:$FULL_SEM_VER \ + --build-arg FULL_SEM_VER_ARG=$FULL_SEM_VER \ + --build-arg ASSEMBLY_SEM_VER_ARG=$FULL_SEM_VER.0 \ + --build-arg ASSEMBLY_SEM_FILE_VER_ARG=$FULL_SEM_VER.0 \ + --build-arg INFORMATIONAL_VERSION_ARG=$FULL_SEM_VER.0 \ + . +``` + +To run the docker image run the following command: +```sh +docker run -d -p 5050:5050 --name mediabrowser-test mediabrowser +``` + +# Config + +The best way to configure the service is through environment variables. Here are the variables: + +* `cookies__secure` (boolean) - set to true when the cookie should only be set to "secure" (meaning https only cookie). The default is true. +* `db__migrateOnBoot` (boolean) - set to true to automatically update the DB schema on boot. The default is true. +* `db__type` (enum / string) - the type of DB: `mySql`, `postgres`, `sqlite`, and `sqlServer`. The default is `sqlite`. +* `db__mySqlConnectionString` (string) - required if `db__type` is `mySql`. +* `db__postgresConnectionString` (string) - required if `db__type` is `postgres`. +* `db__sqliteConnectionString` (string) - required if `db__type` is `sqlite`. The default is to use an in memory SQLite DB file which means if the process restarts then the DB contents are gone. +* `db__sqlServerConnectionString` (string) - required if `db__type` is `sqlServer`. +* `jwt__audience` (string) - this is the audience value for the authentication JWT. The default is `mediaBrowserAudience`. +* `jwt__expiryMinutes` (integer) - this is the number of minutes that the authentication JWT is set to expire and is also used to set the expiration for the cookie that contains the JWT. The default is `60`. +* `jwt__issuer` (string) - this is the issuer value for the authentication JWT. The default is `mediaBrowserIssuer`. +* `jwt__secretKey` (string) - it is important to set this value to a randomized long string (like a password). It is used for the JWT signing algorithm `HS256`. The default is `mediaBrowserSecretKey`. +* `initial__username` (string) - when the system boots it will create a user with this name (if the user doesn't already exists). The default is `admin`. +* `initial__password` (string) - the password to give for `initial__username`. The default is `admin`. +* `media__castDirectory` (string) - the directory path where cast member JPG files are stored. The file name should match name in the DB (i.e. `John Doe.jpg` should match `John Doe`). The default is `/cast`. +* `media__directorsDirectory` (string) - the directory path where director JPG files are stored. The file name should match name in the DB (i.e. `John Doe.jpg` should match `John Doe`). The default is `/directors`. +* `media__genresDirectory` (string) - the directory path where genre JPG files are stored. The file name should match name in the DB (i.e. `Horror.jpg` should match `Horror`). The default is `/genres`. +* `media__producersDirectory` (string) - the directory path where producer JPG files are stored. The file name should match name in the DB (i.e. `John Doe.jpg` should match `John Doe`). The default is `/producers`. +* `media__writersDirectory` (string) - the directory path where writer JPG files are stored. The file name should match name in the DB (i.e. `John Doe.jpg` should match `John Doe`). The default is `/writers`. +* `media__mediaDirectory` (string) - the directory path where media files are stored with their thumbnails and NFO files. The file name should be the MD5 hash (lowercase) of the media file followed by the file extension (i.e. `1a79a4d60de6718e8e5b326e338ae533.mp4`). The thumbnail file should be `1a79a4d60de6718e8e5b326e338ae533.jpg` and optionally the fanart thumbnail should be `1a79a4d60de6718e8e5b326e338ae533-fanart.jpg`. The [NFO file](https://en.wikipedia.org/wiki/.nfo) should be `1a79a4d60de6718e8e5b326e338ae533.nfo`. The default is `/media`. +* `media__syncOnBoot` (boolean) - when true this will scan the media directory for NFO files and import the data from those files into the DB. After the import the service will terminate. +* `stopAfterSync` (boolean) - when true and `media__syncOnBoot` is true then the application will shutdown after importing the files. +* `media__importExtensions` (dictionary) - a dictionary of file extensions and `order`, `mime`, and `ext`. The `order` is used to sort the dictionary so that using the mine for a file can be used to find a corresponding file extension (since it is possible to find multiple file extensions). The `mime` is the corresponding mime type for the file extension. The `ext` is the normalized file extension (i.e. `wave` is normalized to `wav`). +* `media__importDirectory` (string) - a directory where files can be dumped so that they can be imported into the `media__mediaDirectory`. + +# TODO - Features + +* Cast, director, genre, producer, and writer management. +* M3u playlist support. \ No newline at end of file diff --git a/src/Dockerfile b/src/Dockerfile new file mode 100644 index 0000000..bfe89e7 --- /dev/null +++ b/src/Dockerfile @@ -0,0 +1,88 @@ +# Multi-stage Dockerfile for MediaBrowser +# Stage 1: Build the Angular frontend +FROM node:20-alpine AS frontend-build + +WORKDIR /app/frontend + +# Copy package.json and package-lock.json (if available) +COPY MediaBrowser.Frontend/package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy the frontend source code +COPY MediaBrowser.Frontend/ ./ + +# Create the wwwroot directory in the MediaBrowser project +RUN mkdir -p ../MediaBrowser/wwwroot + +# Build the frontend +RUN npm run build + +# Stage 2: Build the .NET application +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS dotnet-build + +WORKDIR /app + +# Copy solution file and project files +COPY MediaBrowser.slnx ./ +COPY MediaBrowser/MediaBrowser.csproj ./MediaBrowser/ +COPY MediaBrowser.Common/MediaBrowser.Common.csproj ./MediaBrowser.Common/ +COPY Migrations/MediaBrowser.MySql/MediaBrowser.MySql.csproj ./Migrations/MediaBrowser.MySql/ +COPY Migrations/MediaBrowser.Postgres/MediaBrowser.Postgres.csproj ./Migrations/MediaBrowser.Postgres/ +COPY Migrations/MediaBrowser.Sqlite/MediaBrowser.Sqlite.csproj ./Migrations/MediaBrowser.Sqlite/ +COPY Migrations/MediaBrowser.SqlServer/MediaBrowser.SqlServer.csproj ./Migrations/MediaBrowser.SqlServer/ + +# Restore dependencies +RUN dotnet restore MediaBrowser/MediaBrowser.csproj + +# Copy the rest of the source code +COPY . ./ + +# Copy the built frontend from the previous stage +COPY --from=frontend-build /app/MediaBrowser/wwwroot ./MediaBrowser/wwwroot/ + +ARG FULL_SEM_VER_ARG +ARG ASSEMBLY_SEM_VER_ARG +ARG ASSEMBLY_SEM_FILE_VER_ARG +ARG INFORMATIONAL_VERSION_ARG + +# Build the application +RUN dotnet publish MediaBrowser/MediaBrowser.csproj -c Release -o /app/publish \ + /p:Version=$FULL_SEM_VER_ARG \ + /p:AssemblyVersion=$ASSEMBLY_SEM_VER_ARG \ + /p:FileVersion=$ASSEMBLY_SEM_FILE_VER_ARG \ + /p:InformationalVersion=$INFORMATIONAL_VERSION_ARG + +# Stage 3: Runtime image +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime + +WORKDIR /app + +# Install ffmpeg and ffprobe +RUN apt-get update && \ + apt-get install -y ffmpeg && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Create a non-root user +RUN useradd -r -s /bin/false appuser + +# Copy the published application +COPY --from=dotnet-build /app/publish ./ + +# Change ownership to the non-root user +RUN chown -R appuser:appuser /app + +# Switch to the non-root user +USER appuser + +# Expose the port the app runs on +EXPOSE 5050 + +# Set environment variables +ENV ASPNETCORE_URLS=http://+:5050 +ENV ASPNETCORE_ENVIRONMENT=Production + +# Start the application +ENTRYPOINT ["dotnet", "MediaBrowser.dll"] \ No newline at end of file diff --git a/src/MediaBrowser.Common/DbConfig.cs b/src/MediaBrowser.Common/DbConfig.cs new file mode 100644 index 0000000..341ba4f --- /dev/null +++ b/src/MediaBrowser.Common/DbConfig.cs @@ -0,0 +1,20 @@ +namespace MediaBrowser; + +[ExcludeFromCodeCoverage(Justification = "POCO")] +public class DbConfig(IConfiguration configuration) +{ + public bool MigrateOnBoot { get; } = bool.Parse(configuration["db:migrateOnBoot"]!); + public string MySqlConnectionString { get; } = configuration["db:mySqlConnectionString"]!; + public string PostgresConnectionString { get; } = configuration["db:postgresConnectionString"]!; + public string SqliteConnectionString { get; } = configuration["db:sqliteConnectionString"]!; + public string SqlServerConnectionString { get; } = configuration["db:sqlServerConnectionString"]!; + public DbType DbType { get; } = Enum.Parse