diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index fdfe4e7..bd6b591 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,25 +1,30 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
{
- "name": "C# (.NET)",
- // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
- "image": "mcr.microsoft.com/devcontainers/dotnet:1-8.0-bookworm",
- // Features to add to the dev container. More info: https://containers.dev/features.
- "features": {
- "ghcr.io/devcontainers-contrib/features/pre-commit:2": {}
- },
- // Use 'forwardPorts' to make a list of ports inside the container available locally.
- // "forwardPorts": [5000, 5001],
- // "portsAttributes": {
- // "5001": {
- // "protocol": "https"
- // }
- // }
- // Use 'postCreateCommand' to run commands after the container is created.
- "postCreateCommand": "sh . ./.devcontainer/postCreateCommand.sh",
- "postStartCommand": "sh . ./.devcontainer/postStartCommand.sh",
- // Configure tool-specific properties.
- // "customizations": {},
- // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
- "remoteUser": "vscode"
+ "name": "C# (.NET)",
+ // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
+ "image": "mcr.microsoft.com/devcontainers/dotnet:1-8.0-bookworm",
+ // Features to add to the dev container. More info: https://containers.dev/features.
+ "features": {
+ "ghcr.io/devcontainers-contrib/features/pre-commit:2": {},
+ "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}
+ },
+ // Use 'forwardPorts' to make a list of ports inside the container available locally.
+ // "forwardPorts": [5000, 5001],
+ // "portsAttributes": {
+ // "5001": {
+ // "protocol": "https"
+ // }
+ // }
+ // Use 'postCreateCommand' to run commands after the container is created.
+ "postCreateCommand": "sh . ./.devcontainer/postCreateCommand.sh",
+ "postStartCommand": "sh . ./.devcontainer/postStartCommand.sh",
+ // Configure tool-specific properties.
+ // "customizations": {},
+ "mounts": [
+ "source=${localEnv:HOME}${localEnv:USERPROFILE}/source/github,target=/workspaces,type=bind,consistency=cached",
+ "source=${env:HOME}${env:USERPROFILE}/.kube,target=/home/vscode/.kube,type=bind"
+ ]
+ // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
+ // "remoteUser": "root"
}
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index ded57b3..24b9b71 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -17,5 +17,4 @@ updates:
- dependency-name: CasCap.Common.*
- dependency-name: coverlet.*
- dependency-name: Microsoft.NET.Test.Sdk
- - dependency-name: Newtonsoft.Json
- dependency-name: xunit.*
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a66efee..ca5cdae 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -30,10 +30,40 @@ jobs:
lint:
uses: f2calv/gha-workflows/.github/workflows/lint.yml@v1
- ci:
- uses: f2calv/gha-workflows/.github/workflows/dotnet-publish-nuget.yml@v1
+ versioning:
+ uses: f2calv/gha-workflows/.github/workflows/gha-release-versioning.yml@v1
with:
- configuration: ${{ github.event.inputs.configuration }}
- push-preview: ${{ github.event.inputs.push-preview }}
- secrets:
- NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
+ tag-prefix: ''
+ tag-and-release: false
+
+ build:
+ runs-on: ubuntu-latest
+ needs: [versioning]
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - uses: f2calv/gha-dotnet-nuget@v2
+ with:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
+ configuration: ${{ inputs.configuration }}
+ push-preview: ${{ inputs.push-preview }}
+ version: ${{ needs.versioning.outputs.version }}
+ solution-name: CasCap.Api.GooglePhotos.Release.sln
+ execute-tests: false #Note: tests fail in Actions due to rate limiting issues from the Google side :/
+ env:
+ GOOGLE_PHOTOS_ACCESS_TOKEN: ${{ secrets.GOOGLE_PHOTOS_ACCESS_TOKEN }}
+
+ release:
+ needs: [versioning, build]
+ if: needs.versioning.outputs.release-exists == 'false'
+ && (github.ref == format('refs/heads/{0}', github.event.repository.default_branch) || inputs.push-preview == 'true')
+ uses: f2calv/gha-workflows/.github/workflows/gha-release-versioning.yml@v1
+ permissions:
+ contents: write
+ with:
+ semVer: ${{ needs.versioning.outputs.version }}
+ tag-prefix: ''
+ move-major-tag: false
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 9af01cc..a52219f 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.6.0
+ rev: v5.0.0
hooks:
- id: check-xml
- id: check-yaml
@@ -16,7 +16,7 @@ repos:
hooks:
- id: check-json5
- repo: https://github.com/igorshubovych/markdownlint-cli
- rev: v0.41.0
+ rev: v0.43.0
hooks:
- id: markdownlint
args: ["--disable", "MD013", "--disable", "MD034", "--"]
diff --git a/CasCap.Api.GooglePhotos.Debug.sln b/CasCap.Api.GooglePhotos.Debug.sln
new file mode 100644
index 0000000..d73842d
--- /dev/null
+++ b/CasCap.Api.GooglePhotos.Debug.sln
@@ -0,0 +1,78 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.12.35309.182
+MinimumVisualStudioVersion = 15.0.26124.0
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CasCap.Api.GooglePhotos", "src\CasCap.Api.GooglePhotos\CasCap.Api.GooglePhotos.csproj", "{2A448BCC-84A1-4512-87B3-2685E40B75C4}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CasCap.Api.GooglePhotos.Tests", "src\CasCap.Api.GooglePhotos.Tests\CasCap.Api.GooglePhotos.Tests.csproj", "{943A9741-D29D-455F-917F-95FF428F80FD}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{1FAD3270-948C-415D-9D2C-63D410E92476}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenericHost", "samples\GenericHost\GenericHost.csproj", "{A6C54B65-6A2D-4BA9-8A11-6097CC351DA5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp", "samples\ConsoleApp\ConsoleApp.csproj", "{CACAD45C-8DAF-4E61-B6F3-1C911656DB9D}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CasCap.Common.Testing", "..\CasCap.Common\src\CasCap.Common.Testing\CasCap.Common.Testing.csproj", "{09BB7730-C89F-4978-BA26-99B347ACA7A4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CasCap.Common.Net", "..\CasCap.Common\src\CasCap.Common.Net\CasCap.Common.Net.csproj", "{B7278559-B5D2-4481-8C12-4C9F2516341A}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CasCap.Common", "CasCap.Common", "{4092EFD9-C0EE-4876-8CEE-AA1289C1AC36}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {2A448BCC-84A1-4512-87B3-2685E40B75C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2A448BCC-84A1-4512-87B3-2685E40B75C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2A448BCC-84A1-4512-87B3-2685E40B75C4}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {2A448BCC-84A1-4512-87B3-2685E40B75C4}.Debug|x64.Build.0 = Debug|Any CPU
+ {2A448BCC-84A1-4512-87B3-2685E40B75C4}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {2A448BCC-84A1-4512-87B3-2685E40B75C4}.Debug|x86.Build.0 = Debug|Any CPU
+ {943A9741-D29D-455F-917F-95FF428F80FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {943A9741-D29D-455F-917F-95FF428F80FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {943A9741-D29D-455F-917F-95FF428F80FD}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {943A9741-D29D-455F-917F-95FF428F80FD}.Debug|x64.Build.0 = Debug|Any CPU
+ {943A9741-D29D-455F-917F-95FF428F80FD}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {943A9741-D29D-455F-917F-95FF428F80FD}.Debug|x86.Build.0 = Debug|Any CPU
+ {A6C54B65-6A2D-4BA9-8A11-6097CC351DA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A6C54B65-6A2D-4BA9-8A11-6097CC351DA5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A6C54B65-6A2D-4BA9-8A11-6097CC351DA5}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A6C54B65-6A2D-4BA9-8A11-6097CC351DA5}.Debug|x64.Build.0 = Debug|Any CPU
+ {A6C54B65-6A2D-4BA9-8A11-6097CC351DA5}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A6C54B65-6A2D-4BA9-8A11-6097CC351DA5}.Debug|x86.Build.0 = Debug|Any CPU
+ {CACAD45C-8DAF-4E61-B6F3-1C911656DB9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CACAD45C-8DAF-4E61-B6F3-1C911656DB9D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CACAD45C-8DAF-4E61-B6F3-1C911656DB9D}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {CACAD45C-8DAF-4E61-B6F3-1C911656DB9D}.Debug|x64.Build.0 = Debug|Any CPU
+ {CACAD45C-8DAF-4E61-B6F3-1C911656DB9D}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {CACAD45C-8DAF-4E61-B6F3-1C911656DB9D}.Debug|x86.Build.0 = Debug|Any CPU
+ {09BB7730-C89F-4978-BA26-99B347ACA7A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {09BB7730-C89F-4978-BA26-99B347ACA7A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {09BB7730-C89F-4978-BA26-99B347ACA7A4}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {09BB7730-C89F-4978-BA26-99B347ACA7A4}.Debug|x64.Build.0 = Debug|Any CPU
+ {09BB7730-C89F-4978-BA26-99B347ACA7A4}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {09BB7730-C89F-4978-BA26-99B347ACA7A4}.Debug|x86.Build.0 = Debug|Any CPU
+ {B7278559-B5D2-4481-8C12-4C9F2516341A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B7278559-B5D2-4481-8C12-4C9F2516341A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B7278559-B5D2-4481-8C12-4C9F2516341A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B7278559-B5D2-4481-8C12-4C9F2516341A}.Debug|x64.Build.0 = Debug|Any CPU
+ {B7278559-B5D2-4481-8C12-4C9F2516341A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B7278559-B5D2-4481-8C12-4C9F2516341A}.Debug|x86.Build.0 = Debug|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {A6C54B65-6A2D-4BA9-8A11-6097CC351DA5} = {1FAD3270-948C-415D-9D2C-63D410E92476}
+ {CACAD45C-8DAF-4E61-B6F3-1C911656DB9D} = {1FAD3270-948C-415D-9D2C-63D410E92476}
+ {09BB7730-C89F-4978-BA26-99B347ACA7A4} = {4092EFD9-C0EE-4876-8CEE-AA1289C1AC36}
+ {B7278559-B5D2-4481-8C12-4C9F2516341A} = {4092EFD9-C0EE-4876-8CEE-AA1289C1AC36}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {315434A5-DC61-4070-8452-C6695E1B14E0}
+ EndGlobalSection
+EndGlobal
diff --git a/CasCap.Api.GooglePhotos.Release.sln b/CasCap.Api.GooglePhotos.Release.sln
new file mode 100644
index 0000000..6835f5c
--- /dev/null
+++ b/CasCap.Api.GooglePhotos.Release.sln
@@ -0,0 +1,66 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.12.35309.182
+MinimumVisualStudioVersion = 15.0.26124.0
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CasCap.Api.GooglePhotos", "src\CasCap.Api.GooglePhotos\CasCap.Api.GooglePhotos.csproj", "{2A448BCC-84A1-4512-87B3-2685E40B75C4}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CasCap.Api.GooglePhotos.Tests", "src\CasCap.Api.GooglePhotos.Tests\CasCap.Api.GooglePhotos.Tests.csproj", "{943A9741-D29D-455F-917F-95FF428F80FD}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{1FAD3270-948C-415D-9D2C-63D410E92476}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenericHost", "samples\GenericHost\GenericHost.csproj", "{A6C54B65-6A2D-4BA9-8A11-6097CC351DA5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp", "samples\ConsoleApp\ConsoleApp.csproj", "{CACAD45C-8DAF-4E61-B6F3-1C911656DB9D}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CasCap.Common", "CasCap.Common", "{4092EFD9-C0EE-4876-8CEE-AA1289C1AC36}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}"
+ ProjectSection(SolutionItems) = preProject
+ Directory.Build.props = Directory.Build.props
+ Directory.Packages.props = Directory.Packages.props
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {2A448BCC-84A1-4512-87B3-2685E40B75C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2A448BCC-84A1-4512-87B3-2685E40B75C4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2A448BCC-84A1-4512-87B3-2685E40B75C4}.Release|x64.ActiveCfg = Release|Any CPU
+ {2A448BCC-84A1-4512-87B3-2685E40B75C4}.Release|x64.Build.0 = Release|Any CPU
+ {2A448BCC-84A1-4512-87B3-2685E40B75C4}.Release|x86.ActiveCfg = Release|Any CPU
+ {2A448BCC-84A1-4512-87B3-2685E40B75C4}.Release|x86.Build.0 = Release|Any CPU
+ {943A9741-D29D-455F-917F-95FF428F80FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {943A9741-D29D-455F-917F-95FF428F80FD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {943A9741-D29D-455F-917F-95FF428F80FD}.Release|x64.ActiveCfg = Release|Any CPU
+ {943A9741-D29D-455F-917F-95FF428F80FD}.Release|x64.Build.0 = Release|Any CPU
+ {943A9741-D29D-455F-917F-95FF428F80FD}.Release|x86.ActiveCfg = Release|Any CPU
+ {943A9741-D29D-455F-917F-95FF428F80FD}.Release|x86.Build.0 = Release|Any CPU
+ {A6C54B65-6A2D-4BA9-8A11-6097CC351DA5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A6C54B65-6A2D-4BA9-8A11-6097CC351DA5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A6C54B65-6A2D-4BA9-8A11-6097CC351DA5}.Release|x64.ActiveCfg = Release|Any CPU
+ {A6C54B65-6A2D-4BA9-8A11-6097CC351DA5}.Release|x64.Build.0 = Release|Any CPU
+ {A6C54B65-6A2D-4BA9-8A11-6097CC351DA5}.Release|x86.ActiveCfg = Release|Any CPU
+ {A6C54B65-6A2D-4BA9-8A11-6097CC351DA5}.Release|x86.Build.0 = Release|Any CPU
+ {CACAD45C-8DAF-4E61-B6F3-1C911656DB9D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CACAD45C-8DAF-4E61-B6F3-1C911656DB9D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CACAD45C-8DAF-4E61-B6F3-1C911656DB9D}.Release|x64.ActiveCfg = Release|Any CPU
+ {CACAD45C-8DAF-4E61-B6F3-1C911656DB9D}.Release|x64.Build.0 = Release|Any CPU
+ {CACAD45C-8DAF-4E61-B6F3-1C911656DB9D}.Release|x86.ActiveCfg = Release|Any CPU
+ {CACAD45C-8DAF-4E61-B6F3-1C911656DB9D}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {A6C54B65-6A2D-4BA9-8A11-6097CC351DA5} = {1FAD3270-948C-415D-9D2C-63D410E92476}
+ {CACAD45C-8DAF-4E61-B6F3-1C911656DB9D} = {1FAD3270-948C-415D-9D2C-63D410E92476}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {315434A5-DC61-4070-8452-C6695E1B14E0}
+ EndGlobalSection
+EndGlobal
diff --git a/CasCap.Apis.GooglePhotos.sln b/CasCap.Apis.GooglePhotos.sln
deleted file mode 100644
index 74e727d..0000000
--- a/CasCap.Apis.GooglePhotos.sln
+++ /dev/null
@@ -1,83 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.29009.5
-MinimumVisualStudioVersion = 15.0.26124.0
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CasCap.Apis.GooglePhotos", "src\CasCap.Apis.GooglePhotos\CasCap.Apis.GooglePhotos.csproj", "{2A448BCC-84A1-4512-87B3-2685E40B75C4}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CasCap.Apis.GooglePhotos.Tests", "src\CasCap.Apis.GooglePhotos.Tests\CasCap.Apis.GooglePhotos.Tests.csproj", "{943A9741-D29D-455F-917F-95FF428F80FD}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{1FAD3270-948C-415D-9D2C-63D410E92476}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleApp", "samples\ConsoleApp\ConsoleApp.csproj", "{2B5E1E09-BAAF-4D66-89BF-D80A1912B6CA}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenericHost", "samples\GenericHost\GenericHost.csproj", "{A6C54B65-6A2D-4BA9-8A11-6097CC351DA5}"
-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
- {2A448BCC-84A1-4512-87B3-2685E40B75C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {2A448BCC-84A1-4512-87B3-2685E40B75C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {2A448BCC-84A1-4512-87B3-2685E40B75C4}.Debug|x64.ActiveCfg = Debug|Any CPU
- {2A448BCC-84A1-4512-87B3-2685E40B75C4}.Debug|x64.Build.0 = Debug|Any CPU
- {2A448BCC-84A1-4512-87B3-2685E40B75C4}.Debug|x86.ActiveCfg = Debug|Any CPU
- {2A448BCC-84A1-4512-87B3-2685E40B75C4}.Debug|x86.Build.0 = Debug|Any CPU
- {2A448BCC-84A1-4512-87B3-2685E40B75C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {2A448BCC-84A1-4512-87B3-2685E40B75C4}.Release|Any CPU.Build.0 = Release|Any CPU
- {2A448BCC-84A1-4512-87B3-2685E40B75C4}.Release|x64.ActiveCfg = Release|Any CPU
- {2A448BCC-84A1-4512-87B3-2685E40B75C4}.Release|x64.Build.0 = Release|Any CPU
- {2A448BCC-84A1-4512-87B3-2685E40B75C4}.Release|x86.ActiveCfg = Release|Any CPU
- {2A448BCC-84A1-4512-87B3-2685E40B75C4}.Release|x86.Build.0 = Release|Any CPU
- {943A9741-D29D-455F-917F-95FF428F80FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {943A9741-D29D-455F-917F-95FF428F80FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {943A9741-D29D-455F-917F-95FF428F80FD}.Debug|x64.ActiveCfg = Debug|Any CPU
- {943A9741-D29D-455F-917F-95FF428F80FD}.Debug|x64.Build.0 = Debug|Any CPU
- {943A9741-D29D-455F-917F-95FF428F80FD}.Debug|x86.ActiveCfg = Debug|Any CPU
- {943A9741-D29D-455F-917F-95FF428F80FD}.Debug|x86.Build.0 = Debug|Any CPU
- {943A9741-D29D-455F-917F-95FF428F80FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {943A9741-D29D-455F-917F-95FF428F80FD}.Release|Any CPU.Build.0 = Release|Any CPU
- {943A9741-D29D-455F-917F-95FF428F80FD}.Release|x64.ActiveCfg = Release|Any CPU
- {943A9741-D29D-455F-917F-95FF428F80FD}.Release|x64.Build.0 = Release|Any CPU
- {943A9741-D29D-455F-917F-95FF428F80FD}.Release|x86.ActiveCfg = Release|Any CPU
- {943A9741-D29D-455F-917F-95FF428F80FD}.Release|x86.Build.0 = Release|Any CPU
- {2B5E1E09-BAAF-4D66-89BF-D80A1912B6CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {2B5E1E09-BAAF-4D66-89BF-D80A1912B6CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {2B5E1E09-BAAF-4D66-89BF-D80A1912B6CA}.Debug|x64.ActiveCfg = Debug|Any CPU
- {2B5E1E09-BAAF-4D66-89BF-D80A1912B6CA}.Debug|x64.Build.0 = Debug|Any CPU
- {2B5E1E09-BAAF-4D66-89BF-D80A1912B6CA}.Debug|x86.ActiveCfg = Debug|Any CPU
- {2B5E1E09-BAAF-4D66-89BF-D80A1912B6CA}.Debug|x86.Build.0 = Debug|Any CPU
- {2B5E1E09-BAAF-4D66-89BF-D80A1912B6CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {2B5E1E09-BAAF-4D66-89BF-D80A1912B6CA}.Release|x64.ActiveCfg = Release|Any CPU
- {2B5E1E09-BAAF-4D66-89BF-D80A1912B6CA}.Release|x64.Build.0 = Release|Any CPU
- {2B5E1E09-BAAF-4D66-89BF-D80A1912B6CA}.Release|x86.ActiveCfg = Release|Any CPU
- {2B5E1E09-BAAF-4D66-89BF-D80A1912B6CA}.Release|x86.Build.0 = Release|Any CPU
- {A6C54B65-6A2D-4BA9-8A11-6097CC351DA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {A6C54B65-6A2D-4BA9-8A11-6097CC351DA5}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {A6C54B65-6A2D-4BA9-8A11-6097CC351DA5}.Debug|x64.ActiveCfg = Debug|Any CPU
- {A6C54B65-6A2D-4BA9-8A11-6097CC351DA5}.Debug|x64.Build.0 = Debug|Any CPU
- {A6C54B65-6A2D-4BA9-8A11-6097CC351DA5}.Debug|x86.ActiveCfg = Debug|Any CPU
- {A6C54B65-6A2D-4BA9-8A11-6097CC351DA5}.Debug|x86.Build.0 = Debug|Any CPU
- {A6C54B65-6A2D-4BA9-8A11-6097CC351DA5}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {A6C54B65-6A2D-4BA9-8A11-6097CC351DA5}.Release|x64.ActiveCfg = Release|Any CPU
- {A6C54B65-6A2D-4BA9-8A11-6097CC351DA5}.Release|x64.Build.0 = Release|Any CPU
- {A6C54B65-6A2D-4BA9-8A11-6097CC351DA5}.Release|x86.ActiveCfg = Release|Any CPU
- {A6C54B65-6A2D-4BA9-8A11-6097CC351DA5}.Release|x86.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(NestedProjects) = preSolution
- {2B5E1E09-BAAF-4D66-89BF-D80A1912B6CA} = {1FAD3270-948C-415D-9D2C-63D410E92476}
- {A6C54B65-6A2D-4BA9-8A11-6097CC351DA5} = {1FAD3270-948C-415D-9D2C-63D410E92476}
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {315434A5-DC61-4070-8452-C6695E1B14E0}
- EndGlobalSection
-EndGlobal
diff --git a/Directory.Build.props b/Directory.Build.props
index 71d50b5..8431bf5 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -2,11 +2,11 @@
CasCap
- 12.0
+ 13.0
+ enable
bf9d717e-ecd3-40e4-850d-14010c167289
- enable
@@ -22,7 +22,7 @@
Alex Vincent
true
- https://github.com/f2calv/CasCap.Apis.GooglePhotos
+ https://github.com/f2calv/CasCap.Api.GooglePhotos
MIT
true
snupkg
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 0000000..a98e2ac
--- /dev/null
+++ b/Directory.Packages.props
@@ -0,0 +1,30 @@
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/README.md b/README.md
index c0a573e..97cb087 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,11 @@
-# CasCap.Apis.GooglePhotos
+# CasCap.Api.GooglePhotos
## _Unofficial_ Google Photos Library API wrapper library for .NET applications
-[cascap.apis.googlephotos-badge]: https://img.shields.io/nuget/v/CasCap.Apis.GooglePhotos?color=blue
-[cascap.apis.googlephotos-url]: https://nuget.org/packages/CasCap.Apis.GooglePhotos
+[CasCap.Api.GooglePhotos-badge]: https://img.shields.io/nuget/v/CasCap.Api.GooglePhotos?color=blue
+[CasCap.Api.GooglePhotos-url]: https://nuget.org/packages/CasCap.Api.GooglePhotos
-![CI](https://github.com/f2calv/CasCap.Apis.GooglePhotos/actions/workflows/ci.yml/badge.svg) [![Coverage Status](https://coveralls.io/repos/github/f2calv/CasCap.Apis.GooglePhotos/badge.svg?branch=main)](https://coveralls.io/github/f2calv/CasCap.Apis.GooglePhotos?branch=main) [![SonarCloud Coverage](https://sonarcloud.io/api/project_badges/measure?project=f2calv_CasCap.Apis.GooglePhotos&metric=code_smells)](https://sonarcloud.io/component_measures/metric/code_smells/list?id=f2calv_CasCap.Apis.GooglePhotos) [![Nuget][cascap.apis.googlephotos-badge]][cascap.apis.googlephotos-url]
+![CI](https://github.com/f2calv/CasCap.Api.GooglePhotos/actions/workflows/ci.yml/badge.svg) [![Coverage Status](https://coveralls.io/repos/github/f2calv/CasCap.Api.GooglePhotos/badge.svg?branch=main)](https://coveralls.io/github/f2calv/CasCap.Api.GooglePhotos?branch=main) [![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=f2calv_CasCap.Api.GooglePhotos&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=f2calv_CasCap.Api.GooglePhotos) [![Nuget][CasCap.Api.GooglePhotos-badge]][CasCap.Api.GooglePhotos-url]
> Want to save yourself some coding? See the _preview_ release of [GooglePhotosCli](https://github.com/f2calv/CasCap.GooglePhotosCli) using this library...
@@ -17,7 +17,7 @@ If you find this library of use then please give it a thumbs-up by giving this r
If you wish to interact with your Google Photos media items/albums then there are official [PHP and Java Client Libraries](https://developers.google.com/photos/library/guides/client-libraries). However if you're looking for a comprehensive .NET library then you were out of luck... until now :)
-The _CasCap.Apis.GooglePhotos_ library wraps up all the available functionality of the Google Photos REST API in easy to use methods.
+The _CasCap.Api.GooglePhotos_ library wraps up all the available functionality of the Google Photos REST API in easy to use methods.
Note: Before you jump in and use this library you should be aware that the [Google Photos Library API](https://developers.google.com/photos/library/reference/rest) has some key limitations. The biggest of these is that the API only allows the upload/addition of images/videos to the library, no edits or deletion are possible and have to be done manually via [https://photos.google.com](https://photos.google.com).
@@ -40,7 +40,7 @@ Using your Google Account the steps are\*;
## Library Configuration/Usage
-Install the package into your project using NuGet ([see details here](https://www.nuget.org/packages/CasCap.Apis.GooglePhotos/)).
+Install the package into your project using NuGet ([see details here](https://www.nuget.org/packages/CasCap.Api.GooglePhotos/)).
For .NET Core applications using dependency injection the primary API usage is to call IServiceCollection.AddGooglePhotos in the Startup.cs ConfigureServices method.
@@ -255,9 +255,9 @@ public class Startup
All API functions are exposed by the GooglePhotosService class. There are several sample .NET Core applications which show the basics on how to set-up/config/use the library;
-- [Console App](https://github.com/f2calv/CasCap.Apis.GooglePhotos/tree/master/samples/ConsoleApp) with no dependency injection.
-- [Console App](https://github.com/f2calv/CasCap.Apis.GooglePhotos/tree/master/samples/GenericHost) using configuration, logging and dependency injection via the [.NET Generic Host](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-3.1).
-- [Integration Test App](https://github.com/f2calv/CasCap.Apis.GooglePhotos/blob/master/src/CasCap.Apis.GooglePhotos.Tests/Tests/Tests.cs) has the majority of the commented examples of various interactions.
+- [Console App](https://github.com/f2calv/CasCap.Api.GooglePhotos/tree/master/samples/ConsoleApp) with no dependency injection.
+- [Console App](https://github.com/f2calv/CasCap.Api.GooglePhotos/tree/master/samples/GenericHost) using configuration, logging and dependency injection via the [.NET Generic Host](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-3.1).
+- [Integration Test App](https://github.com/f2calv/CasCap.Api.GooglePhotos/blob/master/src/CasCap.Api.GooglePhotos.Tests/Tests/Tests.cs) has the majority of the commented examples of various interactions.
### Core Dependencies
@@ -267,20 +267,20 @@ All API functions are exposed by the GooglePhotosService class. There are severa
### Misc Tips
-- The [NuGet package](https://www.nuget.org/packages/CasCap.Apis.GooglePhotos/) includes [SourceLink](https://github.com/dotnet/sourcelink) which enables you to jump inside the library and debug the API yourself. By default Visual Studio 2017/2019 does not allow this and will pop up an message "You are debugging a Release build of...", to disable this message go into the Visual Studio debugging options and un-check the 'Just My Code' option (menu path, Tools > Options > Debugging).
+- The [NuGet package](https://www.nuget.org/packages/CasCap.Api.GooglePhotos/) includes [SourceLink](https://github.com/dotnet/sourcelink) which enables you to jump inside the library and debug the API yourself. By default Visual Studio 2017/2019 does not allow this and will pop up an message "You are debugging a Release build of...", to disable this message go into the Visual Studio debugging options and un-check the 'Just My Code' option (menu path, Tools > Options > Debugging).
### Resources
-- https://developers.google.com/photos
-- https://console.developers.google.com
+-
+-
- [Google Photos Library API](https://developers.google.com/photos)
- [Google Photos Library API REST Reference](https://developers.google.com/photos/library/reference/rest)
- [Google Photos Library API Authorisation Scopes](https://developers.google.com/photos/library/guides/authorization)
### Feedback/Issues
-Please post any issues or feedback [here](https://github.com/f2calv/CasCap.Apis.GooglePhotos/issues).
+Please post any issues or feedback [here](https://github.com/f2calv/CasCap.Api.GooglePhotos/issues).
### License
-CasCap.Apis.GooglePhotos is Copyright © 2020 [Alex Vincent](https://github.com/f2calv) under the [MIT license](LICENSE).
+CasCap.Api.GooglePhotos is Copyright © 2020 [Alex Vincent](https://github.com/f2calv) under the [MIT license](LICENSE).
diff --git a/samples/ConsoleApp/ConsoleApp.csproj b/samples/ConsoleApp/ConsoleApp.csproj
index 7e3e51c..539c2c6 100644
--- a/samples/ConsoleApp/ConsoleApp.csproj
+++ b/samples/ConsoleApp/ConsoleApp.csproj
@@ -2,11 +2,11 @@
Exe
- net8.0
+ net8.0;net9.0
-
+
diff --git a/samples/ConsoleApp/Program.cs b/samples/ConsoleApp/Program.cs
index 4bb4fa4..2a12117 100644
--- a/samples/ConsoleApp/Program.cs
+++ b/samples/ConsoleApp/Program.cs
@@ -1,16 +1,9 @@
-using CasCap.Models;
-using CasCap.Services;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
-using System.Diagnostics;
-using System.Net;
-
-string _user = null;//e.g. "your.email@mydomain.com";
+string _user = null;//e.g. "your.email@mydomain.com";
string _clientId = null;//e.g. "012345678901-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.apps.googleusercontent.com";
string _clientSecret = null;//e.g. "abcabcabcabcabcabcabcabc";
const string _testFolder = "c:/temp/GooglePhotos/";//local folder of test media files
-if (new[] { _user, _clientId, _clientSecret }.Any(p => string.IsNullOrWhiteSpace(p)))
+if (new[] { _user, _clientId, _clientSecret }.Any(string.IsNullOrWhiteSpace))
{
Console.WriteLine("Please populate authentication details to continue...");
Debugger.Break();
@@ -38,7 +31,7 @@
ClientId = _clientId,
ClientSecret = _clientSecret,
//FileDataStoreFullPathOverride = _testFolder,
- Scopes = new[] { GooglePhotosScope.Access, GooglePhotosScope.Sharing },//Access+Sharing == full access
+ Scopes = [GooglePhotosScope.Access, GooglePhotosScope.Sharing],//Access+Sharing == full access
};
//3) (Optional) display local OAuth 2.0 JSON file(s);
@@ -62,17 +55,18 @@
var _googlePhotosSvc = new GooglePhotosService(logger, Options.Create(options), client);
//6) log-in
-if (!await _googlePhotosSvc.LoginAsync()) throw new Exception($"login failed!");
+if (!await _googlePhotosSvc.LoginAsync())
+ throw new GooglePhotosException($"login failed!");
//get existing/create new album
var albumTitle = $"{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}-{Guid.NewGuid()}";//make-up a random title
-var album = await _googlePhotosSvc.GetOrCreateAlbumAsync(albumTitle);
-if (album is null) throw new Exception("album creation failed!");
+var album = await _googlePhotosSvc.GetOrCreateAlbumAsync(albumTitle) ?? throw new GooglePhotosException("album creation failed!");
+
Console.WriteLine($"{nameof(album)} '{album.title}' id is '{album.id}'");
//upload single media item and assign to album
-var mediaItem = await _googlePhotosSvc.UploadSingle($"{_testFolder}test1.jpg", album.id);
-if (mediaItem is null) throw new Exception("media item upload failed!");
+var mediaItem = await _googlePhotosSvc.UploadSingle($"{_testFolder}test1.jpg", album.id) ?? throw new GooglePhotosException("media item upload failed!");
+
Console.WriteLine($"{nameof(mediaItem)} '{mediaItem.mediaItem.filename}' id is '{mediaItem.mediaItem.id}'");
//retrieve all media items in the album
@@ -82,4 +76,4 @@
i++;
Console.WriteLine($"{i}\t{item.filename}\t{item.mediaMetadata.width}x{item.mediaMetadata.height}");
}
-if (i == 0) throw new Exception("retrieve media items by album id failed!");
+if (i == 0) throw new GooglePhotosException("retrieve media items by album id failed!");
diff --git a/samples/ConsoleApp/Properties/launchSettings.json b/samples/ConsoleApp/Properties/launchSettings.json
new file mode 100644
index 0000000..76afbb9
--- /dev/null
+++ b/samples/ConsoleApp/Properties/launchSettings.json
@@ -0,0 +1,10 @@
+{
+ "profiles": {
+ "ConsoleApp": {
+ "commandName": "Project",
+ "environmentVariables": {
+ "NETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/samples/ConsoleApp/Usings.cs b/samples/ConsoleApp/Usings.cs
new file mode 100644
index 0000000..1e04ccb
--- /dev/null
+++ b/samples/ConsoleApp/Usings.cs
@@ -0,0 +1,7 @@
+global using CasCap.Exceptions;
+global using CasCap.Models;
+global using CasCap.Services;
+global using Microsoft.Extensions.Logging;
+global using Microsoft.Extensions.Options;
+global using System.Diagnostics;
+global using System.Net;
diff --git a/samples/GenericHost/GenericHost.csproj b/samples/GenericHost/GenericHost.csproj
index 4fa91b7..b0cfa2f 100644
--- a/samples/GenericHost/GenericHost.csproj
+++ b/samples/GenericHost/GenericHost.csproj
@@ -2,22 +2,21 @@
Exe
- net8.0
+ net8.0;net9.0
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
diff --git a/samples/GenericHost/Program.cs b/samples/GenericHost/Program.cs
index 74007b0..0d60c3d 100644
--- a/samples/GenericHost/Program.cs
+++ b/samples/GenericHost/Program.cs
@@ -1,23 +1,7 @@
-namespace CasCap;
+var builder = Host.CreateApplicationBuilder(args);
-public class Program
-{
- static readonly string _environmentName = "Development";
+builder.Services.AddGooglePhotos(builder.Configuration);
+builder.Services.AddHostedService();
- public static void Main(string[] args) =>
- Host.CreateDefaultBuilder(args)
- .UseEnvironment(_environmentName)
- .ConfigureAppConfiguration((hostContext, configBuilder) =>
- {
- configBuilder.AddJsonFile($"appsettings.json", optional: false, reloadOnChange: true);
- configBuilder.AddJsonFile($"appsettings.{_environmentName}.json", optional: true, reloadOnChange: true);
- if (hostContext.HostingEnvironment.IsDevelopment())
- configBuilder.AddUserSecrets();
- })
- .ConfigureServices((hostContext, services) =>
- {
- services.AddGooglePhotos();
- services.AddHostedService();
- })
- .Build().Run();
-}
+IHost host = builder.Build();
+host.Run();
diff --git a/samples/GenericHost/Properties/launchSettings.json b/samples/GenericHost/Properties/launchSettings.json
new file mode 100644
index 0000000..874eebf
--- /dev/null
+++ b/samples/GenericHost/Properties/launchSettings.json
@@ -0,0 +1,10 @@
+{
+ "profiles": {
+ "GenericHost": {
+ "commandName": "Project",
+ "environmentVariables": {
+ "NETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/samples/GenericHost/Services/MyBackgroundService.cs b/samples/GenericHost/Services/MyBackgroundService.cs
index 1183d7b..4305263 100644
--- a/samples/GenericHost/Services/MyBackgroundService.cs
+++ b/samples/GenericHost/Services/MyBackgroundService.cs
@@ -18,33 +18,33 @@ public MyBackgroundService(ILogger logger, IHostApplication
protected async override Task ExecuteAsync(CancellationToken cancellationToken)
{
- _logger.LogDebug($"starting {nameof(ExecuteAsync)}...");
+ _logger.LogInformation("{serviceName} starting {methodName}...", nameof(MyBackgroundService), nameof(ExecuteAsync));
//log-in
- if (!await _googlePhotosSvc.LoginAsync()) throw new Exception($"login failed!");
+ if (!await _googlePhotosSvc.LoginAsync(cancellationToken)) throw new GooglePhotosException($"login failed!");
//get existing/create new album
var albumTitle = $"{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}-{Guid.NewGuid()}";//make-up a random title
- var album = await _googlePhotosSvc.GetOrCreateAlbumAsync(albumTitle);
- if (album is null) throw new Exception("album creation failed!");
- Console.WriteLine($"{nameof(album)} '{album.title}' id is '{album.id}'");
+ var album = await _googlePhotosSvc.GetOrCreateAlbumAsync(albumTitle) ?? throw new GooglePhotosException("album creation failed!");
+ _logger.LogInformation("{serviceName} {name} '{title}' id is '{id}'", nameof(MyBackgroundService), nameof(album), album.title, album.id);
//upload single media item and assign to album
- var mediaItem = await _googlePhotosSvc.UploadSingle($"{_testFolder}test1.jpg", album.id);
- if (mediaItem is null) throw new Exception("media item upload failed!");
- Console.WriteLine($"{nameof(mediaItem)} '{mediaItem.mediaItem.filename}' id is '{mediaItem.mediaItem.id}'");
+ var path = $"{_testFolder}test1.jpg";
+ var mediaItem = await _googlePhotosSvc.UploadSingle(path, album.id) ?? throw new GooglePhotosException($"media item '{path}' upload failed!");
+ _logger.LogInformation("{serviceName} {name} '{filename}' id is '{id}'",
+ nameof(MyBackgroundService), nameof(mediaItem), mediaItem.mediaItem.filename, mediaItem.mediaItem.id);
//retrieve all media items in the album
- var albumMediaItems = await _googlePhotosSvc.GetMediaItemsByAlbumAsync(album.id, cancellationToken: cancellationToken).ToListAsync();
- if (albumMediaItems is null) throw new Exception("retrieve media items by album id failed!");
+ var albumMediaItems = await _googlePhotosSvc.GetMediaItemsByAlbumAsync(album.id, cancellationToken: cancellationToken).ToListAsync(cancellationToken) ?? throw new GooglePhotosException("retrieve media items by album id failed!");
var i = 1;
foreach (var item in albumMediaItems)
{
- Console.WriteLine($"{i}\t{item.filename}\t{item.mediaMetadata.width}x{item.mediaMetadata.height}");
+ _logger.LogInformation("{serviceName} album #{i} {filename} {width}x{height}", nameof(MyBackgroundService), i, item.filename,
+ item.mediaMetadata.width, item.mediaMetadata.height);
i++;
}
- _logger.LogDebug($"exiting {nameof(ExecuteAsync)}...");
+ _logger.LogInformation("{serviceName} exiting {methodName}...", nameof(MyBackgroundService), nameof(ExecuteAsync));
_appLifetime.StopApplication();
}
}
diff --git a/samples/GenericHost/Usings.cs b/samples/GenericHost/Usings.cs
index c3f8354..2d52bc8 100644
--- a/samples/GenericHost/Usings.cs
+++ b/samples/GenericHost/Usings.cs
@@ -1,4 +1,5 @@
-global using CasCap.Services;
+global using CasCap.Exceptions;
+global using CasCap.Services;
global using Microsoft.Extensions.Configuration;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Hosting;
diff --git a/src/CasCap.Apis.GooglePhotos.Tests/CasCap.Apis.GooglePhotos.Tests.csproj b/src/CasCap.Api.GooglePhotos.Tests/CasCap.Api.GooglePhotos.Tests.csproj
similarity index 69%
rename from src/CasCap.Apis.GooglePhotos.Tests/CasCap.Apis.GooglePhotos.Tests.csproj
rename to src/CasCap.Api.GooglePhotos.Tests/CasCap.Api.GooglePhotos.Tests.csproj
index b94ea12..d73687f 100644
--- a/src/CasCap.Apis.GooglePhotos.Tests/CasCap.Apis.GooglePhotos.Tests.csproj
+++ b/src/CasCap.Api.GooglePhotos.Tests/CasCap.Api.GooglePhotos.Tests.csproj
@@ -1,34 +1,41 @@
- net8.0
+ net8.0;net9.0
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
-
+
diff --git a/src/CasCap.Api.GooglePhotos.Tests/Tests/ExifTests.cs b/src/CasCap.Api.GooglePhotos.Tests/Tests/ExifTests.cs
new file mode 100644
index 0000000..00f8a98
--- /dev/null
+++ b/src/CasCap.Api.GooglePhotos.Tests/Tests/ExifTests.cs
@@ -0,0 +1,103 @@
+//namespace CasCap.Tests;
+
+//public class ExifTests : TestBase
+//{
+// public ExifTests(ITestOutputHelper output) : base(output) { }
+
+// ///
+// /// Minimal exif tags added by Google.
+// ///
+// const int googleExifTagCount = 5;
+
+// [SkipIfCIBuildTheory, Trait("Type", nameof(GooglePhotosService))]
+// [InlineData("test11.jpg", 55.041388888888889d, 8.4677777777777781d, 62)]
+// public async Task CheckExifData(string fileName, double latitude, double longitude, int exifTagCount)
+// {
+// var path = $"{_testFolder}{fileName}";
+// var originalBytes = File.ReadAllBytes(path);
+
+// var loginResult = await _googlePhotosSvc.LoginAsync();
+// Assert.True(loginResult);
+
+// var tplOriginal = await GetExifInfo(path);
+// Assert.Equal(latitude, tplOriginal.latitude);
+// Assert.Equal(longitude, tplOriginal.longitude);
+// Assert.Equal(exifTagCount, tplOriginal.exifTagCount);
+
+// var uploadToken = await _googlePhotosSvc.UploadMediaAsync(path, GooglePhotosUploadMethod.Simple);
+// Assert.NotNull(uploadToken);
+// var newMediaItemResult = await _googlePhotosSvc.AddMediaItemAsync(uploadToken, path);
+// Assert.NotNull(newMediaItemResult);
+// //the upload returns a null baseUrl
+// Assert.Null(newMediaItemResult.mediaItem.baseUrl);
+
+// //so now retrieve all media items
+// var mediaItems = await _googlePhotosSvc.GetMediaItemsAsync().ToListAsync();
+
+// var uploadedMediaItem = mediaItems.FirstOrDefault(p => p.filename.Equals(fileName));
+// Assert.NotNull(uploadedMediaItem);
+// Assert.True(uploadedMediaItem.isPhoto);
+
+// var bytesNoExif = await _googlePhotosSvc.DownloadBytes(uploadedMediaItem, includeExifMetadata: false);
+// Assert.NotNull(bytesNoExif);
+// var tplNoExif = await ExifTests.GetExifInfo(bytesNoExif);
+// Assert.True(googleExifTagCount == tplNoExif.exifTagCount);
+
+// var bytesWithExif = await _googlePhotosSvc.DownloadBytes(uploadedMediaItem, includeExifMetadata: true);
+// Assert.NotNull(bytesWithExif);
+// var tplWithExif = await ExifTests.GetExifInfo(bytesWithExif);
+// Assert.Null(tplWithExif.latitude);//location exif data always stripped :(
+// Assert.Null(tplWithExif.longitude);//location exif data always stripped :(
+// Assert.True(tplOriginal.exifTagCount > tplWithExif.exifTagCount);//due to Google-stripping fewer exif tags are returned
+// Assert.True(googleExifTagCount < tplWithExif.exifTagCount);
+// }
+
+// static async Task<(double? latitude, double? longitude, int exifTagCount)> GetExifInfo(string path)
+// {
+// using var image = await Image.LoadAsync(path);
+// return GetLatLong(image);
+// }
+
+// static async Task<(double? latitude, double? longitude, int exifTagCount)> GetExifInfo(byte[] bytes)
+// {
+// var stream = new MemoryStream(bytes);
+// using var image = await Image.LoadAsync(stream);
+// return GetLatLong(image);
+// }
+
+// static (double? latitude, double? longitude, int exifTagCount) GetLatLong(Image image)
+// {
+// double? latitude = null, longitude = null;
+// var exifTagCount = image.Metadata.ExifProfile?.Values.Count ?? 0;
+// if (image.Metadata.ExifProfile.Values?.Any() ?? false)
+// {
+// var exifData = image.Metadata.ExifProfile;
+// if (exifData != null)
+// {
+// if (exifData.TryGetValue(ExifTag.GPSLatitude, out var gpsLatitude)
+// && exifData.TryGetValue(ExifTag.GPSLatitudeRef, out var gpsLatitudeRef))
+// latitude = GetCoordinates(gpsLatitudeRef.ToString(), gpsLatitude.Value);
+
+// if (exifData.TryGetValue(ExifTag.GPSLongitude, out var gpsLong)
+// && exifData.TryGetValue(ExifTag.GPSLongitudeRef, out var gpsLongRef))
+// longitude = GetCoordinates(gpsLongRef.ToString(), gpsLong.Value);
+
+// Debug.WriteLine($"latitude,longitude = {latitude},{longitude}");
+// }
+// }
+
+// return (latitude, longitude, exifTagCount);
+// }
+
+// static double GetCoordinates(string gpsRef, Rational[] rationals)
+// {
+// var degrees = rationals[0].Numerator / rationals[0].Denominator;
+// var minutes = rationals[1].Numerator / rationals[1].Denominator;
+// var seconds = rationals[2].Numerator / rationals[2].Denominator;
+
+// var coordinate = degrees + (minutes / 60d) + (seconds / 3600d);
+// if (gpsRef == "S" || gpsRef == "W")
+// coordinate *= -1;
+// return coordinate;
+// }
+//}
diff --git a/src/CasCap.Apis.GooglePhotos.Tests/Tests/TestBase.cs b/src/CasCap.Api.GooglePhotos.Tests/Tests/TestBase.cs
similarity index 80%
rename from src/CasCap.Apis.GooglePhotos.Tests/Tests/TestBase.cs
rename to src/CasCap.Api.GooglePhotos.Tests/Tests/TestBase.cs
index fbf1018..87727b9 100644
--- a/src/CasCap.Apis.GooglePhotos.Tests/Tests/TestBase.cs
+++ b/src/CasCap.Api.GooglePhotos.Tests/Tests/TestBase.cs
@@ -1,9 +1,4 @@
-using CasCap.Services;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
-using Xunit.Abstractions;
-namespace CasCap.Apis.GooglePhotos.Tests;
+namespace CasCap.Tests;
public abstract class TestBase
{
@@ -29,7 +24,7 @@ public TestBase(ITestOutputHelper output)
_logger = ApplicationLogging.LoggerFactory.CreateLogger();
//add services
- services.AddGooglePhotos();
+ services.AddGooglePhotos(configuration);
//retrieve services
var serviceProvider = services.BuildServiceProvider();
diff --git a/src/CasCap.Apis.GooglePhotos.Tests/Tests/Tests.cs b/src/CasCap.Api.GooglePhotos.Tests/Tests/Tests.cs
similarity index 78%
rename from src/CasCap.Apis.GooglePhotos.Tests/Tests/Tests.cs
rename to src/CasCap.Api.GooglePhotos.Tests/Tests/Tests.cs
index 99a5083..e97ef15 100644
--- a/src/CasCap.Apis.GooglePhotos.Tests/Tests/Tests.cs
+++ b/src/CasCap.Api.GooglePhotos.Tests/Tests/Tests.cs
@@ -1,35 +1,46 @@
-using CasCap.Common.Extensions;
-using CasCap.Models;
-using CasCap.Services;
-using CasCap.Xunit;
-using System.Diagnostics;
-using Xunit;
-using Xunit.Abstractions;
-namespace CasCap.Apis.GooglePhotos.Tests;
+namespace CasCap.Tests;
///
-/// Integration tests for GooglePhotos API library, update appsettings.Test.json with appropriate login values before running.
+/// Integration tests for CasCap.Api.GooglePhotos library.
+/// For local testing update appsettings.Test.json and/or add values to UserSecrets before running.
+///
+/// When running integration tests under GitHub Actions you should first run the tests locally with the test account
+/// and then update the GitHub Actions secret to the access_token from the local JSON file, e.g.
+/// C:\Users\???\AppData\Roaming\Google.Apis.Auth\Google.Apis.Auth.OAuth2.Responses.TokenResponse-???@???.com
+/// This is because the current method of authentication used by CasCap.Api.GooglePhotos requires
+/// browser interaction which is not possible during CI.
///
-public class Tests : TestBase
+public class Tests(ITestOutputHelper output) : TestBase(output)
{
- public Tests(ITestOutputHelper output) : base(output) { }
-
[SkipIfCIBuildFact]
- public async Task LoginTest()
+ public async Task DoLogin()
{
- var loginResult = await _googlePhotosSvc.LoginAsync();
- Assert.True(loginResult);
+ if (IsCI())
+ {
+ var accessToken = Environment.GetEnvironmentVariable("GOOGLE_PHOTOS_ACCESS_TOKEN");
+ if (string.IsNullOrWhiteSpace(accessToken)) throw new ArgumentNullException(nameof(accessToken));
+ _googlePhotosSvc.SetAuth("Bearer", accessToken);
+ return true;
+ }
+ else
+ {
+ Assert.True(true);
+ return await _googlePhotosSvc.LoginAsync();
+ }
}
+ static bool IsCI() => Environment.GetEnvironmentVariable("TF_BUILD") is not null
+ || Environment.GetEnvironmentVariable("GITHUB_ACTIONS") is not null;
+
static string GetRandomAlbumName() => $"{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}";
- [SkipIfCIBuildTheory, Trait("Type", nameof(GooglePhotosService))]
+ [Theory, Trait("Type", nameof(GooglePhotosService))]
[InlineData(GooglePhotosUploadMethod.Simple)]
[InlineData(GooglePhotosUploadMethod.ResumableSingle)]
[InlineData(GooglePhotosUploadMethod.ResumableMultipart)]
public async Task UploadMediaTests(GooglePhotosUploadMethod uploadMethod)
{
- var loginResult = await _googlePhotosSvc.LoginAsync();
+ var loginResult = await DoLogin();
Assert.True(loginResult);
var paths = Directory.GetFiles(_testFolder);
@@ -44,12 +55,12 @@ public async Task UploadMediaTests(GooglePhotosUploadMethod uploadMethod)
}
}
- [SkipIfCIBuildTheory]
+ [Theory]
[InlineData("test1.jpg", "test2.jpg")]
[InlineData("test1.jpg", "Урок-английского-10.jpg")]
public async Task UploadSingleTests(string file1, string file2)
{
- var loginResult = await _googlePhotosSvc.LoginAsync();
+ var loginResult = await DoLogin();
Assert.True(loginResult);
//upload single media item
@@ -82,13 +93,13 @@ public async Task UploadSingleTests(string file1, string file2)
//retrieve all media items from album
var albumMediaItems = await _googlePhotosSvc.GetMediaItemsByAlbumAsync(album.id).ToListAsync();
Assert.NotNull(albumMediaItems);
- Assert.True(albumMediaItems.Count == 1);
+ Assert.Single(albumMediaItems);
}
- [SkipIfCIBuildFact]
+ [Fact]
public async Task UploadMultipleTests()
{
- var loginResult = await _googlePhotosSvc.LoginAsync();
+ var loginResult = await DoLogin();
Assert.True(loginResult);
//upload multiple media items
@@ -145,18 +156,18 @@ public async Task UploadMultipleTests()
ids.Add("invalid-id");
var mediaItems2 = await _googlePhotosSvc.GetMediaItemsByIdsAsync(ids.ToArray()).ToListAsync();
Assert.NotNull(mediaItems2);
- Assert.True(ids.Count - mediaItems2.Count == 1);//should have 1 failed item
+ Assert.Equal(1, ids.Count - mediaItems2.Count);//should have 1 failed item
foreach (var _mi in mediaItems2)
{
- Debug.WriteLine(_mi.ToJSON());
+ Debug.WriteLine(_mi.ToJson());
}
Assert.True(true);
}
- [SkipIfCIBuildFact]
+ [Fact]
public async Task FilteringTests()
{
- var loginResult = await _googlePhotosSvc.LoginAsync();
+ var loginResult = await DoLogin();
Assert.True(loginResult);
contentFilter contentFilter = null;
@@ -165,19 +176,19 @@ public async Task FilteringTests()
contentFilter = new contentFilter
#pragma warning restore CS0162 // Unreachable code detected
{
- includedContentCategories = new[] { GooglePhotosContentCategoryType.PEOPLE },
- //includedContentCategories = new[] { contentCategoryType.WEDDINGS },
- //excludedContentCategories = new[] { contentCategoryType.PEOPLE }
+ includedContentCategories = [GooglePhotosContentCategoryType.PEOPLE],
+ //includedContentCategories = [contentCategoryType.WEDDINGS],
+ //excludedContentCategories = [contentCategoryType.PEOPLE]
};
dateFilter dateFilter = new()
{
- //dates = new gDate[] { new gDate { year = 2020 } },
- //dates = new gDate[] { new gDate { year = 2016 } },
- //dates = new gDate[] { new gDate { year = 2016, month = 12 } },
- //dates = new gDate[] { new gDate { year = 2016, month = 12, day = 16 } },
- //ranges = new gDateRange[] { new gDateRange { startDate = new startDate { year = 2016 }, endDate = new endDate { year = 2017 } } },
- ranges = new gDateRange[] { new gDateRange { startDate = new gDate { year = 1900 }, endDate = new gDate { year = DateTime.UtcNow.Year } } },
+ //dates = [new() { year = 2020 }],
+ //dates = [new() { year = 2016 }],
+ //dates = [new() { year = 2016, month = 12 }],
+ //dates = [new() { year = 2016, month = 12, day = 16 }],
+ //ranges = [new() { startDate = new() { year = 2016 }, endDate = new() { year = 2017 } }],
+ ranges = [new() { startDate = new() { year = 1900 }, endDate = new() { year = DateTime.UtcNow.Year } }],
};
mediaTypeFilter mediaTypeFilter = null;
if (false)
@@ -185,8 +196,8 @@ public async Task FilteringTests()
mediaTypeFilter = new mediaTypeFilter
#pragma warning restore CS0162 // Unreachable code detected
{
- mediaTypes = new[] { GooglePhotosMediaType.PHOTO }
- //mediaTypes = new[] { mediaType.VIDEO }
+ mediaTypes = [GooglePhotosMediaType.PHOTO]
+ //mediaTypes = [mediaType.VIDEO]
};
featureFilter featureFilter = null;
if (false)
@@ -194,7 +205,7 @@ public async Task FilteringTests()
featureFilter = new featureFilter
#pragma warning restore CS0162 // Unreachable code detected
{
- includedFeatures = new[] { GooglePhotosFeatureType.FAVORITES }
+ includedFeatures = [GooglePhotosFeatureType.FAVORITES]
};
var filter = new Filter
{
@@ -214,10 +225,10 @@ public async Task FilteringTests()
Assert.True(true);
}
- [SkipIfCIBuildFact]
+ [Fact]
public async Task EnrichmentsTests()
{
- var loginResult = await _googlePhotosSvc.LoginAsync();
+ var loginResult = await DoLogin();
Assert.True(loginResult);
var path = $"{_testFolder}test7.jpg";
@@ -254,10 +265,10 @@ public async Task EnrichmentsTests()
Assert.NotNull(enrichmentId2);
}
- [SkipIfCIBuildFact]
+ [Fact]
public async Task SharingTests()
{
- var loginResult = await _googlePhotosSvc.LoginAsync();
+ var loginResult = await DoLogin();
Assert.True(loginResult);
//get or create new album
@@ -276,7 +287,7 @@ public async Task SharingTests()
//get album contents
var mediaItems1 = await _googlePhotosSvc.GetMediaItemsByAlbumAsync(album.id).ToListAsync();
Assert.NotNull(mediaItems1);
- Assert.True(mediaItems1.Count == 1);
+ Assert.Single(mediaItems1);
//remove from album
var result2 = await _googlePhotosSvc.RemoveMediaItemsFromAlbumAsync(album.id, new[] { mediaItem.mediaItem.id });
@@ -285,7 +296,7 @@ public async Task SharingTests()
//get album contents
var mediaItems2 = await _googlePhotosSvc.GetMediaItemsByAlbumAsync(album.id).ToListAsync();
Assert.NotNull(mediaItems2);
- Assert.True(mediaItems2.Count == 0);
+ Assert.Empty(mediaItems2);
//re-add same pic to album
var result3 = await _googlePhotosSvc.AddMediaItemsToAlbumAsync(album.id, new[] { mediaItem.mediaItem.id });
@@ -294,7 +305,7 @@ public async Task SharingTests()
//get album contents
var mediaItems3 = await _googlePhotosSvc.GetMediaItemsByAlbumAsync(album.id).ToListAsync();
Assert.NotNull(mediaItems3);
- Assert.True(mediaItems3.Count == 1);
+ Assert.Single(mediaItems3);
//enable sharing on album
var shareInfo = await _googlePhotosSvc.ShareAlbumAsync(album.id);
@@ -304,7 +315,7 @@ public async Task SharingTests()
//retrieve shared albums
var sharedAlbums = await _googlePhotosSvc.GetSharedAlbumsAsync();
- Assert.True(sharedAlbums.Count == 1);
+ Assert.Single(sharedAlbums);
var sharedAlb1a = await _googlePhotosSvc.GetAlbumAsync(album.id);
Assert.NotNull(sharedAlb1a);
@@ -317,8 +328,8 @@ public async Task SharingTests()
Assert.True(result4);
}
- //[SkipIfCIBuildFact]
- [SkipIfCIBuildTheory]
+ //[Fact]
+ [Theory]
[InlineData(1, 10)]
[InlineData(1, 100)]
[InlineData(2, 10)]
@@ -330,15 +341,15 @@ public async Task SharingTests()
[InlineData(20, 10)]
public async Task DownloadBytesTests(int pageSize, int maxPageCount)
{
- var expectedCount = Directory.GetFiles(_testFolder).Length;
+ //var expectedCount = Directory.GetFiles(_testFolder).Length;
- var loginResult = await _googlePhotosSvc.LoginAsync();
+ var loginResult = await DoLogin();
Assert.True(loginResult);
var mediaItems = await _googlePhotosSvc.GetMediaItemsAsync(pageSize, maxPageCount).ToListAsync();
Assert.NotNull(mediaItems);
Assert.True(mediaItems.Count > 0, "no media items returned!");
- Assert.True(mediaItems.Select(p => p.id).Distinct().Count() == expectedCount, $"inaccurate list of media items returned, expected {expectedCount} but returned {mediaItems.Count}");
+ //Assert.True(mediaItems.Select(p => p.id).Distinct().Count() == expectedCount, $"inaccurate list of media items returned, expected {expectedCount} but returned {mediaItems.Count}");
var bytes = await _googlePhotosSvc.DownloadBytes(mediaItems[0]);
Assert.NotNull(bytes);
diff --git a/src/CasCap.Api.GooglePhotos.Tests/Usings.cs b/src/CasCap.Api.GooglePhotos.Tests/Usings.cs
new file mode 100644
index 0000000..fdf648d
--- /dev/null
+++ b/src/CasCap.Api.GooglePhotos.Tests/Usings.cs
@@ -0,0 +1,12 @@
+global using CasCap.Common.Extensions;
+global using CasCap.Models;
+global using CasCap.Services;
+global using CasCap.Xunit;
+global using Microsoft.Extensions.Configuration;
+global using Microsoft.Extensions.DependencyInjection;
+global using Microsoft.Extensions.Logging;
+global using SixLabors.ImageSharp;
+global using SixLabors.ImageSharp.Metadata.Profiles.Exif;
+global using System.Diagnostics;
+global using Xunit;
+global using Xunit.Abstractions;
diff --git a/src/CasCap.Apis.GooglePhotos.Tests/appsettings.Test.json b/src/CasCap.Api.GooglePhotos.Tests/appsettings.Test.json
similarity index 100%
rename from src/CasCap.Apis.GooglePhotos.Tests/appsettings.Test.json
rename to src/CasCap.Api.GooglePhotos.Tests/appsettings.Test.json
diff --git a/src/CasCap.Apis.GooglePhotos.Tests/testdata/test.mp4 b/src/CasCap.Api.GooglePhotos.Tests/testdata/test.mp4
similarity index 100%
rename from src/CasCap.Apis.GooglePhotos.Tests/testdata/test.mp4
rename to src/CasCap.Api.GooglePhotos.Tests/testdata/test.mp4
diff --git a/src/CasCap.Apis.GooglePhotos.Tests/testdata/test0.jpg b/src/CasCap.Api.GooglePhotos.Tests/testdata/test0.jpg
similarity index 100%
rename from src/CasCap.Apis.GooglePhotos.Tests/testdata/test0.jpg
rename to src/CasCap.Api.GooglePhotos.Tests/testdata/test0.jpg
diff --git a/src/CasCap.Apis.GooglePhotos.Tests/testdata/test1.jpg b/src/CasCap.Api.GooglePhotos.Tests/testdata/test1.jpg
similarity index 100%
rename from src/CasCap.Apis.GooglePhotos.Tests/testdata/test1.jpg
rename to src/CasCap.Api.GooglePhotos.Tests/testdata/test1.jpg
diff --git a/src/CasCap.Apis.GooglePhotos.Tests/testdata/test11.jpg b/src/CasCap.Api.GooglePhotos.Tests/testdata/test11.jpg
similarity index 100%
rename from src/CasCap.Apis.GooglePhotos.Tests/testdata/test11.jpg
rename to src/CasCap.Api.GooglePhotos.Tests/testdata/test11.jpg
diff --git a/src/CasCap.Apis.GooglePhotos.Tests/testdata/test2.jpg b/src/CasCap.Api.GooglePhotos.Tests/testdata/test2.jpg
similarity index 100%
rename from src/CasCap.Apis.GooglePhotos.Tests/testdata/test2.jpg
rename to src/CasCap.Api.GooglePhotos.Tests/testdata/test2.jpg
diff --git a/src/CasCap.Apis.GooglePhotos.Tests/testdata/test3.jpg b/src/CasCap.Api.GooglePhotos.Tests/testdata/test3.jpg
similarity index 100%
rename from src/CasCap.Apis.GooglePhotos.Tests/testdata/test3.jpg
rename to src/CasCap.Api.GooglePhotos.Tests/testdata/test3.jpg
diff --git a/src/CasCap.Apis.GooglePhotos.Tests/testdata/test4.jpg b/src/CasCap.Api.GooglePhotos.Tests/testdata/test4.jpg
similarity index 100%
rename from src/CasCap.Apis.GooglePhotos.Tests/testdata/test4.jpg
rename to src/CasCap.Api.GooglePhotos.Tests/testdata/test4.jpg
diff --git a/src/CasCap.Apis.GooglePhotos.Tests/testdata/test5.jpg b/src/CasCap.Api.GooglePhotos.Tests/testdata/test5.jpg
similarity index 100%
rename from src/CasCap.Apis.GooglePhotos.Tests/testdata/test5.jpg
rename to src/CasCap.Api.GooglePhotos.Tests/testdata/test5.jpg
diff --git a/src/CasCap.Apis.GooglePhotos.Tests/testdata/test6.jpg b/src/CasCap.Api.GooglePhotos.Tests/testdata/test6.jpg
similarity index 100%
rename from src/CasCap.Apis.GooglePhotos.Tests/testdata/test6.jpg
rename to src/CasCap.Api.GooglePhotos.Tests/testdata/test6.jpg
diff --git a/src/CasCap.Apis.GooglePhotos.Tests/testdata/test7.jpg b/src/CasCap.Api.GooglePhotos.Tests/testdata/test7.jpg
similarity index 100%
rename from src/CasCap.Apis.GooglePhotos.Tests/testdata/test7.jpg
rename to src/CasCap.Api.GooglePhotos.Tests/testdata/test7.jpg
diff --git a/src/CasCap.Apis.GooglePhotos.Tests/testdata/test8.jpg b/src/CasCap.Api.GooglePhotos.Tests/testdata/test8.jpg
similarity index 100%
rename from src/CasCap.Apis.GooglePhotos.Tests/testdata/test8.jpg
rename to src/CasCap.Api.GooglePhotos.Tests/testdata/test8.jpg
diff --git a/src/CasCap.Apis.GooglePhotos.Tests/testdata/test9.jpg b/src/CasCap.Api.GooglePhotos.Tests/testdata/test9.jpg
similarity index 100%
rename from src/CasCap.Apis.GooglePhotos.Tests/testdata/test9.jpg
rename to src/CasCap.Api.GooglePhotos.Tests/testdata/test9.jpg
diff --git "a/src/CasCap.Apis.GooglePhotos.Tests/testdata/\320\243\321\200\320\276\320\272-\320\260\320\275\320\263\320\273\320\270\320\271\321\201\320\272\320\276\320\263\320\276-10.jpg" "b/src/CasCap.Api.GooglePhotos.Tests/testdata/\320\243\321\200\320\276\320\272-\320\260\320\275\320\263\320\273\320\270\320\271\321\201\320\272\320\276\320\263\320\276-10.jpg"
similarity index 100%
rename from "src/CasCap.Apis.GooglePhotos.Tests/testdata/\320\243\321\200\320\276\320\272-\320\260\320\275\320\263\320\273\320\270\320\271\321\201\320\272\320\276\320\263\320\276-10.jpg"
rename to "src/CasCap.Api.GooglePhotos.Tests/testdata/\320\243\321\200\320\276\320\272-\320\260\320\275\320\263\320\273\320\270\320\271\321\201\320\272\320\276\320\263\320\276-10.jpg"
diff --git a/src/CasCap.Apis.GooglePhotos/Interfaces/IPagingToken.cs b/src/CasCap.Api.GooglePhotos/Abstractions/IPagingToken.cs
similarity index 83%
rename from src/CasCap.Apis.GooglePhotos/Interfaces/IPagingToken.cs
rename to src/CasCap.Api.GooglePhotos/Abstractions/IPagingToken.cs
index 3995a30..b4057a7 100644
--- a/src/CasCap.Apis.GooglePhotos/Interfaces/IPagingToken.cs
+++ b/src/CasCap.Api.GooglePhotos/Abstractions/IPagingToken.cs
@@ -1,4 +1,4 @@
-namespace CasCap.Interfaces;
+namespace CasCap.Abstractions;
public interface IPagingToken
{
diff --git a/src/CasCap.Apis.GooglePhotos/CasCap.Apis.GooglePhotos.csproj b/src/CasCap.Api.GooglePhotos/CasCap.Api.GooglePhotos.csproj
similarity index 54%
rename from src/CasCap.Apis.GooglePhotos/CasCap.Apis.GooglePhotos.csproj
rename to src/CasCap.Api.GooglePhotos/CasCap.Api.GooglePhotos.csproj
index 57a0f84..536cc7e 100644
--- a/src/CasCap.Apis.GooglePhotos/CasCap.Apis.GooglePhotos.csproj
+++ b/src/CasCap.Api.GooglePhotos/CasCap.Api.GooglePhotos.csproj
@@ -1,7 +1,7 @@
- net8.0
+ net8.0;net9.0
true
enable
@@ -19,18 +19,31 @@
For more details about the underlying API see the official site, https://developers.google.com/photos
- For usage examples see the docs on github, https://github.com/f2calv/CasCap.Apis.GooglePhotos
+ For usage examples see the docs on github, https://github.com/f2calv/CasCap.Api.GooglePhotos
google, photos, rest, api, wrapper
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/CasCap.Api.GooglePhotos/Exceptions/GooglePhotosException.cs b/src/CasCap.Api.GooglePhotos/Exceptions/GooglePhotosException.cs
new file mode 100644
index 0000000..4e57c98
--- /dev/null
+++ b/src/CasCap.Api.GooglePhotos/Exceptions/GooglePhotosException.cs
@@ -0,0 +1,23 @@
+using System.Runtime.Serialization;
+
+namespace CasCap.Exceptions;
+
+[Serializable]
+public class GooglePhotosException : Exception
+{
+ public GooglePhotosException() { }
+ public GooglePhotosException(string message) : base(message) { }
+ public GooglePhotosException(string message, Exception? innerException) : base(message, innerException) { }
+
+ public GooglePhotosException(Error error)
+ : base(error is not null && error.error is not null && error.error.message is not null ? error.error.message : "unknown") { }
+
+ [Obsolete("added to pass sonarqube", DiagnosticId = "SYSLIB0051")]
+ protected GooglePhotosException(SerializationInfo info, StreamingContext context) : base(info, context) { }
+
+ [Obsolete("added to pass sonarqube", DiagnosticId = "SYSLIB0051")]
+ public override void GetObjectData(SerializationInfo info, StreamingContext context)
+ {
+ base.GetObjectData(info, context);
+ }
+}
diff --git a/src/CasCap.Api.GooglePhotos/Extensions/DI.cs b/src/CasCap.Api.GooglePhotos/Extensions/DI.cs
new file mode 100644
index 0000000..41a5622
--- /dev/null
+++ b/src/CasCap.Api.GooglePhotos/Extensions/DI.cs
@@ -0,0 +1,83 @@
+using System.Net;
+using System.Net.Http.Headers;
+
+namespace Microsoft.Extensions.DependencyInjection;
+
+public static class DI
+{
+ public static void AddGooglePhotos(this IServiceCollection services, IConfiguration configuration, string sectionKey = GooglePhotosOptions.SectionKey)
+ => services.AddServices(configuration: configuration, sectionKey: sectionKey);
+
+ public static void AddGooglePhotos(this IServiceCollection services, GooglePhotosOptions googlePhotosOptions)
+ => services.AddServices(googlePhotosOptions: googlePhotosOptions);
+
+ public static void AddGooglePhotos(this IServiceCollection services, Action configureOptions)
+ => services.AddServices(configureOptions: configureOptions);
+
+ static void AddServices(this IServiceCollection services,
+ IConfiguration? configuration = null,
+ string sectionKey = GooglePhotosOptions.SectionKey,
+ GooglePhotosOptions? googlePhotosOptions = null,
+ Action? configureOptions = null
+ )
+ {
+ if (configuration is not null)
+ {
+ var configSection = configuration.GetSection(sectionKey);
+ googlePhotosOptions = configSection.Get();
+ if (googlePhotosOptions is not null)
+ services.Configure(configSection);
+ }
+ else if (googlePhotosOptions is not null)
+ {
+ var options = Options.Options.Create(googlePhotosOptions);
+ services.AddSingleton(options);
+ }
+ else if (configureOptions is not null)
+ {
+ services.Configure(configureOptions);
+ googlePhotosOptions = new();
+ configureOptions.Invoke(googlePhotosOptions);
+ }
+ if (googlePhotosOptions is null)
+ throw new GooglePhotosException($"configuration object {nameof(GooglePhotosOptions)} is null so cannot continue");
+
+ services.AddHttpClient((s, client) =>
+ {
+ client.BaseAddress = new Uri(googlePhotosOptions.BaseAddress);
+ client.DefaultRequestHeaders.Add("User-Agent", $"{nameof(CasCap)}.{AppDomain.CurrentDomain.FriendlyName}.{Environment.MachineName}");
+ client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+ client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip"));
+ client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("deflate"));
+ client.Timeout = Timeout.InfiniteTimeSpan;
+ }).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
+ {
+ AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
+ })
+ //https://github.com/aspnet/AspNetCore/issues/6804
+ .SetHandlerLifetime(Timeout.InfiniteTimeSpan)
+ .AddStandardResilienceHandler((options) =>
+ {
+ //RateLimiter
+ options.TotalRequestTimeout = new Http.Resilience.HttpTimeoutStrategyOptions
+ {
+ Timeout = TimeSpan.FromSeconds(90)
+ };
+ //Retry
+ options.Retry = new Http.Resilience.HttpRetryStrategyOptions
+ {
+ MaxRetryAttempts = 6
+ };
+ //Circuit Breaker
+ options.CircuitBreaker = new Http.Resilience.HttpCircuitBreakerStrategyOptions
+ {
+ SamplingDuration = TimeSpan.FromSeconds(180)
+ };
+ //AttemptTimeout
+ options.AttemptTimeout = new Http.Resilience.HttpTimeoutStrategyOptions
+ {
+ Timeout = TimeSpan.FromSeconds(90)
+ };
+ });
+ }
+}
diff --git a/src/CasCap.Apis.GooglePhotos/Messages/Responses.cs b/src/CasCap.Api.GooglePhotos/Messages/Responses.cs
similarity index 85%
rename from src/CasCap.Apis.GooglePhotos/Messages/Responses.cs
rename to src/CasCap.Api.GooglePhotos/Messages/Responses.cs
index 21f5b41..951eefb 100644
--- a/src/CasCap.Apis.GooglePhotos/Messages/Responses.cs
+++ b/src/CasCap.Api.GooglePhotos/Messages/Responses.cs
@@ -22,23 +22,17 @@ public class mediaItemsCreateResponse//todo: should this be internal?
///
/// https://developers.google.com/photos/library/reference/rest/v1/albums/addEnrichment#request-body
///
-internal class AddEnrichmentRequest
+internal class AddEnrichmentRequest(NewEnrichmentItem newEnrichmentItem, AlbumPosition albumPosition)
{
- public AddEnrichmentRequest(NewEnrichmentItem newEnrichmentItem, AlbumPosition albumPosition)
- {
- this.newEnrichmentItem = newEnrichmentItem;
- this.albumPosition = albumPosition;
- }
-
///
/// The enrichment to be added.
///
- public NewEnrichmentItem newEnrichmentItem { get; set; }
+ public NewEnrichmentItem newEnrichmentItem { get; set; } = newEnrichmentItem;
///
/// The position in the album where the enrichment is to be inserted.
///
- public AlbumPosition albumPosition { get; set; }
+ public AlbumPosition albumPosition { get; set; } = albumPosition;
}
///
diff --git a/src/CasCap.Apis.GooglePhotos/Models/GooglePhotos.cs b/src/CasCap.Api.GooglePhotos/Models/GooglePhotos.cs
similarity index 89%
rename from src/CasCap.Apis.GooglePhotos/Models/GooglePhotos.cs
rename to src/CasCap.Api.GooglePhotos/Models/GooglePhotos.cs
index d07a900..c9abe7b 100644
--- a/src/CasCap.Apis.GooglePhotos/Models/GooglePhotos.cs
+++ b/src/CasCap.Api.GooglePhotos/Models/GooglePhotos.cs
@@ -1,8 +1,6 @@
-using Newtonsoft.Json;
-using Newtonsoft.Json.Converters;
-namespace CasCap.Models;
+namespace CasCap.Models;
-[JsonConverter(typeof(StringEnumConverter))]
+[JsonConverter(typeof(JsonStringEnumConverter))]
public enum GooglePhotosScope
{
///
@@ -26,12 +24,12 @@ public enum GooglePhotosScope
///
/// Read access to media items and albums created by the developer. For more information, see Access media items and List library contents, albums, and media items.
///
- /// Intended to be requested together with the.appendonly scope.
+ /// Intended to be requested together with the AppendOnly scope.
///
AppCreatedData,
///
- /// Access to both the .appendonly and .readonly scopes. Doesn't include .sharing.
+ /// Access to both the AppendOnly and ReadOnly scopes. Doesn't include Sharing scope.
///
Access,
@@ -43,7 +41,7 @@ public enum GooglePhotosScope
Sharing
}
-[JsonConverter(typeof(StringEnumConverter))]
+[JsonConverter(typeof(JsonStringEnumConverter))]
public enum GooglePhotosUploadMethod
{
Simple,
@@ -51,7 +49,7 @@ public enum GooglePhotosUploadMethod
ResumableMultipart
}
-[JsonConverter(typeof(StringEnumConverter))]
+[JsonConverter(typeof(JsonStringEnumConverter))]
public enum GooglePhotosPositionType
{
///
@@ -80,20 +78,20 @@ public enum GooglePhotosPositionType
AFTER_ENRICHMENT_ITEM
}
-[JsonConverter(typeof(StringEnumConverter))]
+[JsonConverter(typeof(JsonStringEnumConverter))]
public enum GooglePhotosMediaType
{
PHOTO,
VIDEO
}
-[JsonConverter(typeof(StringEnumConverter))]
+[JsonConverter(typeof(JsonStringEnumConverter))]
public enum GooglePhotosFeatureType
{
FAVORITES
}
-[JsonConverter(typeof(StringEnumConverter))]
+[JsonConverter(typeof(JsonStringEnumConverter))]
public enum GooglePhotosContentCategoryType
{
ANIMALS,
@@ -132,15 +130,15 @@ public Filter(DateTime startDate, DateTime endDate)
{
dateFilter = new dateFilter
{
- ranges = new[] { new gDateRange { startDate = new gDate(startDate), endDate = new gDate(endDate) } }
+ ranges = [new() { startDate = new gDate(startDate), endDate = new gDate(endDate) }]
};
}
- public Filter(GooglePhotosContentCategoryType category) => this.contentFilter = new contentFilter { includedContentCategories = new[] { category } };
+ public Filter(GooglePhotosContentCategoryType category) => contentFilter = new contentFilter { includedContentCategories = [category] };
- public Filter(GooglePhotosContentCategoryType[] categories) => this.contentFilter = new contentFilter { includedContentCategories = categories };
+ public Filter(GooglePhotosContentCategoryType[] categories) => contentFilter = new contentFilter { includedContentCategories = categories };
- public Filter(List categories) => this.contentFilter = new contentFilter { includedContentCategories = categories.ToArray() };
+ public Filter(List categories) => contentFilter = new contentFilter { includedContentCategories = categories.ToArray() };
public contentFilter? contentFilter { get; set; }
public dateFilter? dateFilter { get; set; }
@@ -201,6 +199,7 @@ public class gDateRange
public gDate endDate { get; set; } = default!;
}
+[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
public class Album
{
///
@@ -236,7 +235,7 @@ public class Album
///
/// [Output only] The number of media items in the album.
///
- public int mediaItemsCount { get; set; }
+ public long? mediaItemsCount { get; set; }
///
/// [Output only] Information related to shared albums.This field is only populated if the album is a shared album, the developer created the album and the user has granted the photoslibrary.sharing scope.
@@ -442,100 +441,72 @@ public class EnrichmentItem
/// Text for this enrichment item.
/// https://developers.google.com/photos/library/reference/rest/v1/albums/addEnrichment#textenrichment
///
-public class TextEnrichment
+public class TextEnrichment(string text)
{
- public TextEnrichment(string text)
- {
- this.text = text;
- }
-
- public string text { get; set; }
+ public string text { get; set; } = text;
}
///
/// An enrichment containing a single location.
/// https://developers.google.com/photos/library/reference/rest/v1/albums/addEnrichment#locationenrichment
///
-public class LocationEnrichment
+public class LocationEnrichment(Location location)
{
- public LocationEnrichment(Location location)
- {
- this.location = location;
- }
-
///
/// Location for this enrichment item.
///
- public Location location { get; set; }
+ public Location location { get; set; } = location;
}
///
/// Represents a physical location.
/// https://developers.google.com/photos/library/reference/rest/v1/albums/addEnrichment#location
///
-public class Location
+public class Location(string locationName, Latlng latlng)
{
- public Location(string locationName, Latlng latlng)
- {
- this.locationName = locationName;
- this.latlng = latlng;
- }
-
///
/// Name of the location to be displayed.
///
- public string locationName { get; set; }
+ public string locationName { get; set; } = locationName;
///
/// Position of the location on the map.
///
- public Latlng latlng { get; set; }
+ public Latlng latlng { get; set; } = latlng;
}
///
/// An object representing a latitude/longitude pair. This is expressed as a pair of doubles representing degrees latitude and degrees longitude. Unless specified otherwise, this must conform to the WGS84 standard. Values must be within normalized ranges.
/// https://developers.google.com/photos/library/reference/rest/v1/albums/addEnrichment#latlng
///
-public class Latlng
+public class Latlng(double latitude, double longitude)
{
- public Latlng(double latitude, double longitude)
- {
- this.latitude = latitude;
- this.longitude = longitude;
- }
-
///
/// The latitude in degrees. It must be in the range [-90.0, +90.0].
///
- public double latitude { get; set; }
+ public double latitude { get; set; } = latitude;
///
/// The longitude in degrees. It must be in the range [-180.0, +180.0].
///
- public double longitude { get; set; }
+ public double longitude { get; set; } = longitude;
}
///
/// An enrichment containing a map, showing origin and destination locations.
/// https://developers.google.com/photos/library/reference/rest/v1/albums/addEnrichment#mapenrichment
///
-public class MapEnrichment
+public class MapEnrichment(Location origin, Location destination)
{
- public MapEnrichment(Location origin, Location destination)
- {
- this.origin = origin;
- this.destination = destination;
- }
-
///
/// Origin location for this enrichment item.
///
- public Location origin { get; set; }
+ public Location origin { get; set; } = origin;
///
/// Destination location for this enrichment item.
///
- public Location destination { get; set; }
+ public Location destination { get; set; } = destination;
}
#endregion
diff --git a/src/CasCap.Apis.GooglePhotos/Models/GooglePhotosOptions.cs b/src/CasCap.Api.GooglePhotos/Models/GooglePhotosOptions.cs
similarity index 91%
rename from src/CasCap.Apis.GooglePhotos/Models/GooglePhotosOptions.cs
rename to src/CasCap.Api.GooglePhotos/Models/GooglePhotosOptions.cs
index 8f15f50..498c90c 100644
--- a/src/CasCap.Apis.GooglePhotos/Models/GooglePhotosOptions.cs
+++ b/src/CasCap.Api.GooglePhotos/Models/GooglePhotosOptions.cs
@@ -3,6 +3,11 @@
[Serializable]
public class GooglePhotosOptions
{
+ ///
+ /// Configuration sub-section locator key.
+ ///
+ public const string SectionKey = $"{nameof(CasCap)}:{nameof(GooglePhotosOptions)}";
+
///
/// The default endpoint for REST API requests, currently defaults to REST API v1.0
///
diff --git a/src/CasCap.Api.GooglePhotos/Models/PagingEventArgs.cs b/src/CasCap.Api.GooglePhotos/Models/PagingEventArgs.cs
new file mode 100644
index 0000000..9ebc53f
--- /dev/null
+++ b/src/CasCap.Api.GooglePhotos/Models/PagingEventArgs.cs
@@ -0,0 +1,10 @@
+namespace CasCap.Models;
+
+public class PagingEventArgs(int pageSize, int pageNumber, int recordCount) : EventArgs
+{
+ public int pageSize { get; } = pageSize;
+ public int pageNumber { get; } = pageNumber;
+ public int recordCount { get; } = recordCount;
+ public DateTime? minDate { get; set; }
+ public DateTime? maxDate { get; set; }
+}
diff --git a/src/CasCap.Apis.GooglePhotos/Models/RequestUris.cs b/src/CasCap.Api.GooglePhotos/Models/RequestUris.cs
similarity index 100%
rename from src/CasCap.Apis.GooglePhotos/Models/RequestUris.cs
rename to src/CasCap.Api.GooglePhotos/Models/RequestUris.cs
diff --git a/src/CasCap.Api.GooglePhotos/Models/UploadProgressEventArgs.cs b/src/CasCap.Api.GooglePhotos/Models/UploadProgressEventArgs.cs
new file mode 100644
index 0000000..313a6dc
--- /dev/null
+++ b/src/CasCap.Api.GooglePhotos/Models/UploadProgressEventArgs.cs
@@ -0,0 +1,10 @@
+namespace CasCap.Models;
+
+public class UploadProgressEventArgs(string fileName, long totalBytes, int batchIndex, long uploadedBytes, long batchSize) : EventArgs
+{
+ public string fileName { get; } = fileName;
+ public long totalBytes { get; } = totalBytes;
+ public long batchIndex { get; } = batchIndex;
+ public long uploadedBytes { get; } = uploadedBytes;
+ public long batchSize { get; } = batchSize;
+}
diff --git a/src/CasCap.Apis.GooglePhotos/Services/Base/GooglePhotosServiceBase.cs b/src/CasCap.Api.GooglePhotos/Services/Base/GooglePhotosServiceBase.cs
similarity index 93%
rename from src/CasCap.Apis.GooglePhotos/Services/Base/GooglePhotosServiceBase.cs
rename to src/CasCap.Api.GooglePhotos/Services/Base/GooglePhotosServiceBase.cs
index d6ef6b6..d0fd3a2 100644
--- a/src/CasCap.Apis.GooglePhotos/Services/Base/GooglePhotosServiceBase.cs
+++ b/src/CasCap.Api.GooglePhotos/Services/Base/GooglePhotosServiceBase.cs
@@ -26,7 +26,7 @@ public abstract class GooglePhotosServiceBase : HttpClientBase
const int defaultBatchSizeMediaItems = 50;
- GooglePhotosOptions? _options;
+ GooglePhotosOptions _options;
public GooglePhotosServiceBase(ILogger logger,
IOptions options,
@@ -34,15 +34,15 @@ HttpClient client
)
{
_logger = logger;
- _options = options.Value;// ?? throw new ArgumentNullException(nameof(options), $"{nameof(GooglePhotosOptions)} cannot be null!");
+ _options = options.Value;
_client = client ?? throw new ArgumentNullException(nameof(client), $"{nameof(HttpClient)} cannot be null!");
}
protected virtual void RaisePagingEvent(PagingEventArgs args) => PagingEvent?.Invoke(this, args);
public event EventHandler? PagingEvent;
- protected virtual void RaiseUploadProgressEvent(UploadProgressArgs args) => UploadProgressEvent?.Invoke(this, args);
- public event EventHandler? UploadProgressEvent;
+ protected virtual void RaiseUploadProgressEvent(UploadProgressEventArgs args) => UploadProgressEvent?.Invoke(this, args);
+ public event EventHandler? UploadProgressEvent;
public static bool IsFileUploadable(string path) => IsFileUploadableByExtension(Path.GetExtension(path));
@@ -105,7 +105,7 @@ public static bool IsFileUploadableByExtension(string extension)
{ GooglePhotosScope.Sharing, "https://www.googleapis.com/auth/photoslibrary.sharing" }
};
- public async Task LoginAsync(string User, string ClientId, string ClientSecret, GooglePhotosScope[] Scopes, string? FileDataStoreFullPathOverride = null)
+ public async Task LoginAsync(string User, string ClientId, string ClientSecret, GooglePhotosScope[] Scopes, string? FileDataStoreFullPathOverride = null, CancellationToken cancellationToken = default)
{
_options = new GooglePhotosOptions
{
@@ -115,22 +115,21 @@ public async Task LoginAsync(string User, string ClientId, string ClientSe
Scopes = Scopes,
FileDataStoreFullPathOverride = FileDataStoreFullPathOverride
};
- return await LoginAsync();
+ return await LoginAsync(cancellationToken);
}
- public async Task LoginAsync(GooglePhotosOptions options)
+ public async Task LoginAsync(GooglePhotosOptions options, CancellationToken cancellationToken = default)
{
_options = options ?? throw new ArgumentNullException(nameof(options), $"{nameof(GooglePhotosOptions)} cannot be null!");
- return await LoginAsync();
+ return await LoginAsync(cancellationToken);
}
- public async Task LoginAsync()
+ public async Task LoginAsync(CancellationToken cancellationToken = default)
{
- if (_options is null) throw new ArgumentNullException(nameof(_options), $"{nameof(GooglePhotosOptions)}.{nameof(_options)} cannot be null!");
- if (string.IsNullOrWhiteSpace(_options.User)) throw new ArgumentNullException(nameof(_options.User), $"{nameof(GooglePhotosOptions)}.{nameof(_options.User)} cannot be null!");
- if (string.IsNullOrWhiteSpace(_options.ClientId)) throw new ArgumentNullException(nameof(_options.ClientId), $"{nameof(GooglePhotosOptions)}.{nameof(_options.ClientId)} cannot be null!");
- if (string.IsNullOrWhiteSpace(_options.ClientSecret)) throw new ArgumentNullException(nameof(_options.ClientSecret), $"{nameof(GooglePhotosOptions)}.{nameof(_options.ClientSecret)} cannot be null!");
- if (_options.Scopes.IsNullOrEmpty()) throw new ArgumentNullException(nameof(_options.Scopes), $"{nameof(GooglePhotosOptions)}.{nameof(_options.Scopes)} cannot be null/empty!");
+ if (string.IsNullOrWhiteSpace(_options.User)) throw new GooglePhotosException($"{nameof(GooglePhotosOptions)}.{nameof(_options.User)} cannot be null!");
+ if (string.IsNullOrWhiteSpace(_options.ClientId)) throw new GooglePhotosException($"{nameof(GooglePhotosOptions)}.{nameof(_options.ClientId)} cannot be null!");
+ if (string.IsNullOrWhiteSpace(_options.ClientSecret)) throw new GooglePhotosException($"{nameof(GooglePhotosOptions)}.{nameof(_options.ClientSecret)} cannot be null!");
+ if (_options.Scopes.IsNullOrEmpty()) throw new GooglePhotosException($"{nameof(GooglePhotosOptions)}.{nameof(_options.Scopes)} cannot be null/empty!");
var secrets = new ClientSecrets { ClientId = _options.ClientId, ClientSecret = _options.ClientSecret };
@@ -143,10 +142,10 @@ public async Task LoginAsync()
secrets,
GetScopes(),
_options.User,
- CancellationToken.None,
+ cancellationToken,
dataStore);
- _logger.LogDebug("Authorisation granted or not required (if the saved access token already available)");
+ _logger.LogDebug("Authorization granted or not required (if the saved access token already available)");
if (credential.Token.IsStale)
{
@@ -161,7 +160,7 @@ public async Task LoginAsync()
}
else
_logger.LogDebug("The access token is OK, continue");
- _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(credential.Token.TokenType, credential.Token.AccessToken);
+ SetAuth(credential.Token.TokenType, credential.Token.AccessToken);
return true;
string[] GetScopes()//todo: make extension method to convert any enum to string[] and move to CasCap.Common.Extensions
@@ -174,6 +173,14 @@ string[] GetScopes()//todo: make extension method to convert any enum to string[
}
}
+ ///
+ /// Workaround to allow setting the auth header when running integration tests from CI.
+ ///
+ ///
+ ///
+ public void SetAuth(string tokenType, string accessToken)
+ => _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(tokenType, accessToken);
+
#region https://photoslibrary.googleapis.com/v1/albums
//https://photoslibrary.googleapis.com/v1/albums/{albumId}
@@ -224,8 +231,8 @@ public Task> GetSharedAlbumsAsync(int pageSize = defaultPageSizeAlbu
else if (tpl.result is not null)//to hide nullability warning
{
var batch = new List(pageSize);
- if (!tpl.result.albums.IsNullOrEmpty()) batch = tpl.result.albums ?? new List();
- if (!tpl.result.sharedAlbums.IsNullOrEmpty()) batch = tpl.result.sharedAlbums ?? new List();
+ if (!tpl.result.albums.IsNullOrEmpty()) batch = tpl.result.albums ?? [];
+ if (!tpl.result.sharedAlbums.IsNullOrEmpty()) batch = tpl.result.sharedAlbums ?? [];
l.AddRange(batch);
if (!string.IsNullOrWhiteSpace(tpl.result.nextPageToken))
RaisePagingEvent(new PagingEventArgs(batch.Count, pageNumber, l.Count));
@@ -332,7 +339,7 @@ async IAsyncEnumerable _GetMediaItemsAsync(int pageSize, int maxPageC
if (pageSize < minPageSizeMediaItems || pageSize > maxPageSizeMediaItems)
throw new ArgumentOutOfRangeException($"{nameof(pageSize)} must be between {minPageSizeMediaItems} and {maxPageSizeMediaItems}!");
- //Note: mediaitem results are not garuanteed to be unique so we check returned ids in a volatile hashset
+ //Note: MediaItem results are not guaranteed to be unique so we check returned ids in a volatile HashSet
var hs = new HashSet();
var pageToken = string.Empty;
var pageNumber = 1;
@@ -344,14 +351,14 @@ async IAsyncEnumerable _GetMediaItemsAsync(int pageSize, int maxPageC
else if (tpl.result is not null)
{
var batch = new List(pageSize);
- if (!tpl.result.mediaItems.IsNullOrEmpty()) batch = tpl.result.mediaItems ?? new List();
+ if (!tpl.result.mediaItems.IsNullOrEmpty()) batch = tpl.result.mediaItems ?? [];
foreach (var mi in batch)
if (!hs.Contains(mi.id))
{
hs.Add(mi.id);
yield return mi;
}
- if (!string.IsNullOrWhiteSpace(tpl.result.nextPageToken) && batch.Any())
+ if (!string.IsNullOrWhiteSpace(tpl.result.nextPageToken) && batch.Count != 0)
{
//Note: low page sizes can return 0 records but still return a continuation token, weirdness
RaisePagingEvent(new PagingEventArgs(batch.Count, pageNumber, hs.Count)
@@ -388,14 +395,14 @@ async IAsyncEnumerable _GetMediaItemsViaPOSTAsync(string? albumId, in
else if (tpl.result is not null)
{
var batch = new List(pageSize);
- if (!tpl.result.mediaItems.IsNullOrEmpty()) batch = tpl.result.mediaItems ?? new List();
+ if (!tpl.result.mediaItems.IsNullOrEmpty()) batch = tpl.result.mediaItems ?? [];
foreach (var mi in batch)
if (!hs.Contains(mi.id))
{
hs.Add(mi.id);
yield return mi;
}
- if (!string.IsNullOrWhiteSpace(tpl.result.nextPageToken) && batch.Any())
+ if (!string.IsNullOrWhiteSpace(tpl.result.nextPageToken) && batch.Count != 0)
RaisePagingEvent(new PagingEventArgs(batch.Count, pageNumber, hs.Count)
{
minDate = batch.Min(p => p.mediaMetadata.creationTime),
@@ -433,6 +440,7 @@ public async IAsyncEnumerable GetMediaItemsByIdsAsync(List me
var batches = mediaItemIds.GetBatches(defaultBatchSizeMediaItems);
foreach (var batch in batches)
{
+ if (cancellationToken.IsCancellationRequested) break;
//see https://github.com/dotnet/aspnetcore/issues/7945 can't use QueryHelpers.AddQueryString
//var queryParams = new Dictionary(batch.Value.Length);
//foreach (var mediaItemId in batch.Value)
@@ -597,7 +605,7 @@ IAsyncEnumerable _GetMediaItemsByFilterAsync(Filter filter, int maxPa
if (!File.Exists(path)) throw new FileNotFoundException($"can't find '{path}'");
var size = new FileInfo(path).Length;
- if (size < 1) throw new Exception($"media file {path} has no data?");
+ if (size < 1) throw new GooglePhotosException($"media file {path} has no data?");
if (IsImage(Path.GetExtension(path)) && size > maxSizeImageBytes)
throw new NotSupportedException($"Media file {path} is too big for known upload limits of {maxSizeImageBytes} bytes!");
if (IsVideo(Path.GetExtension(path)) && size > maxSizeVideoBytes)
@@ -613,7 +621,7 @@ IAsyncEnumerable _GetMediaItemsByFilterAsync(Filter filter, int maxPa
{
headers.Add((X_Goog_Upload_Command, "start"));
var fileName = Path.GetFileName(path);
- //Note: UrlPathEncode below is not intended to be used... but fixes https://github.com/f2calv/CasCap.Apis.GooglePhotos/issues/110
+ //Note: UrlPathEncode below is not intended to be used... but fixes https://github.com/f2calv/CasCap.Api.GooglePhotos/issues/110
headers.Add((X_Goog_Upload_File_Name, HttpUtility.UrlPathEncode(fileName)));
headers.Add((X_Goog_Upload_Protocol, "resumable"));
headers.Add((X_Goog_Upload_Raw_Size, size.ToString()));
@@ -622,22 +630,22 @@ IAsyncEnumerable _GetMediaItemsByFilterAsync(Filter filter, int maxPa
if (uploadMethod == GooglePhotosUploadMethod.Simple)
{
var bytes = File.ReadAllBytes(path);
- var tpl = await PostBytes(RequestUris.uploads, uploadMethod == GooglePhotosUploadMethod.ResumableSingle ? Array.Empty() : bytes, headers: headers);
+ var tpl = await PostBytes(RequestUris.uploads, uploadMethod == GooglePhotosUploadMethod.ResumableSingle ? [] : bytes, headers: headers);
if (tpl.error is not null) throw new GooglePhotosException(tpl.error);
return tpl.result;
}
else
{
- var tpl = await PostBytes(RequestUris.uploads, Array.Empty(), headers: headers);
+ var tpl = await PostBytes(RequestUris.uploads, [], headers: headers);
var status = tpl.responseHeaders.TryGetValue(X_Goog_Upload_Status);
- var Upload_URL = tpl.responseHeaders.TryGetValue(X_Goog_Upload_URL) ?? throw new Exception($"{nameof(X_Goog_Upload_URL)}");
+ var Upload_URL = tpl.responseHeaders.TryGetValue(X_Goog_Upload_URL) ?? throw new GooglePhotosException($"{nameof(X_Goog_Upload_URL)}");
//Debug.WriteLine($"{Upload_URL}={Upload_URL}");
var sUpload_Chunk_Granularity = tpl.responseHeaders.TryGetValue(X_Goog_Upload_Chunk_Granularity);
if (int.TryParse(sUpload_Chunk_Granularity, out var Upload_Chunk_Granularity) && Upload_Chunk_Granularity <= 0)
- throw new Exception($"invalid {X_Goog_Upload_Chunk_Granularity}!");
+ throw new GooglePhotosException($"invalid {X_Goog_Upload_Chunk_Granularity}!");
- headers = new List<(string name, string value)>();
+ headers = [];
if (uploadMethod == GooglePhotosUploadMethod.ResumableSingle)
{
@@ -650,10 +658,10 @@ IAsyncEnumerable _GetMediaItemsByFilterAsync(Filter filter, int maxPa
if (tpl.httpStatusCode != HttpStatusCode.OK)
{
//we were interrupted so query the status of the last upload
- headers = new List<(string name, string value)>
- {
+ headers =
+ [
(X_Goog_Upload_Command, "query")
- };
+ ];
tpl = await PostBytes(Upload_URL, bytes, headers: headers);
if (tpl.error is not null) throw new GooglePhotosException(tpl.error);
@@ -682,11 +690,11 @@ IAsyncEnumerable _GetMediaItemsByFilterAsync(Filter filter, int maxPa
//var lastChunk = offset + Upload_Chunk_Granularity >= size;
var lastChunk = batchIndex + 1 == batchCount;
- headers = new List<(string name, string value)>
- {
+ headers =
+ [
(X_Goog_Upload_Command, $"upload{(lastChunk ? ", finalize" : string.Empty)}"),
(X_Goog_Upload_Offset, offset.ToString())
- };
+ ];
//todo: need to test resuming failed uploads
var bytes = reader.ReadBytes(Upload_Chunk_Granularity);
@@ -696,12 +704,12 @@ IAsyncEnumerable _GetMediaItemsByFilterAsync(Filter filter, int maxPa
if (tpl.httpStatusCode != HttpStatusCode.OK)
{
//we were interrupted so query the status of the last upload
- headers = new List<(string name, string value)>
- {
+ headers =
+ [
(X_Goog_Upload_Command, "query")
- };
+ ];
_logger.LogDebug($"");
- tpl = await PostBytes(Upload_URL, Array.Empty(), headers: headers);
+ tpl = await PostBytes(Upload_URL, [], headers: headers);
status = tpl.responseHeaders.TryGetValue(X_Goog_Upload_Status);
_logger.LogTrace("{methodName}, status={status}", nameof(UploadMediaAsync), status);
@@ -713,7 +721,7 @@ IAsyncEnumerable _GetMediaItemsByFilterAsync(Filter filter, int maxPa
{
attemptCount = 0;//reset retry count
offset += bytes.Length;
- RaiseUploadProgressEvent(new UploadProgressArgs(Path.GetFileName(path), size, batchIndex, offset, bytes.Length));
+ RaiseUploadProgressEvent(new UploadProgressEventArgs(Path.GetFileName(path), size, batchIndex, offset, bytes.Length));
batchIndex++;
//if (callback is not null)
// callback(bytes.Length);
diff --git a/src/CasCap.Apis.GooglePhotos/Services/GooglePhotosService.cs b/src/CasCap.Api.GooglePhotos/Services/GooglePhotosService.cs
similarity index 94%
rename from src/CasCap.Apis.GooglePhotos/Services/GooglePhotosService.cs
rename to src/CasCap.Api.GooglePhotos/Services/GooglePhotosService.cs
index c7eb13f..d8fd8f8 100644
--- a/src/CasCap.Apis.GooglePhotos/Services/GooglePhotosService.cs
+++ b/src/CasCap.Api.GooglePhotos/Services/GooglePhotosService.cs
@@ -5,16 +5,9 @@
///
//https://developers.google.com/photos/library/guides/get-started
//https://developers.google.com/photos/library/guides/authentication-authorization
-public class GooglePhotosService : GooglePhotosServiceBase
+public class GooglePhotosService(ILogger logger, IOptions options, HttpClient client)
+ : GooglePhotosServiceBase(logger, options, client)
{
- public GooglePhotosService(ILogger logger,
- IOptions options,
- HttpClient client
- ) : base(logger, options, client)
- {
-
- }
-
public async Task GetOrCreateAlbumAsync(string title, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
{
var album = await GetAlbumByTitleAsync(title, comparisonType);
diff --git a/src/CasCap.Apis.GooglePhotos/Usings.cs b/src/CasCap.Api.GooglePhotos/Usings.cs
similarity index 67%
rename from src/CasCap.Apis.GooglePhotos/Usings.cs
rename to src/CasCap.Api.GooglePhotos/Usings.cs
index 234333a..e3d3a82 100644
--- a/src/CasCap.Apis.GooglePhotos/Usings.cs
+++ b/src/CasCap.Api.GooglePhotos/Usings.cs
@@ -1,9 +1,10 @@
-global using CasCap.Common.Extensions;
+global using CasCap.Abstractions;
+global using CasCap.Common.Extensions;
global using CasCap.Exceptions;
-global using CasCap.Interfaces;
global using CasCap.Messages;
global using CasCap.Models;
global using CasCap.Services;
global using Microsoft.Extensions.Configuration;
global using Microsoft.Extensions.Logging;
global using Microsoft.Extensions.Options;
+global using System.Text.Json.Serialization;
diff --git a/src/CasCap.Apis.GooglePhotos.Tests/Tests/ExifTests.cs b/src/CasCap.Apis.GooglePhotos.Tests/Tests/ExifTests.cs
deleted file mode 100644
index fe01b77..0000000
--- a/src/CasCap.Apis.GooglePhotos.Tests/Tests/ExifTests.cs
+++ /dev/null
@@ -1,112 +0,0 @@
-using CasCap.Models;
-using CasCap.Services;
-using CasCap.Xunit;
-using SixLabors.ImageSharp;
-using SixLabors.ImageSharp.Metadata.Profiles.Exif;
-using System.Diagnostics;
-using Xunit;
-using Xunit.Abstractions;
-
-namespace CasCap.Apis.GooglePhotos.Tests;
-
-public class ExifTests : TestBase
-{
- public ExifTests(ITestOutputHelper output) : base(output) { }
-
- ///
- /// Minimal exif tags added by Google.
- ///
- const int googleExifTagCount = 5;
-
- [SkipIfCIBuildTheory, Trait("Type", nameof(GooglePhotosService))]
- [InlineData("test11.jpg", 55.041388888888889d, 8.4677777777777781d, 62)]
- public async Task CheckExifData(string fileName, double latitude, double longitude, int exifTagCount)
- {
- var path = $"{_testFolder}{fileName}";
- var originalBytes = File.ReadAllBytes(path);
-
- var loginResult = await _googlePhotosSvc.LoginAsync();
- Assert.True(loginResult);
-
- var tplOriginal = await ExifTests.GetExifInfo(path);
- Assert.Equal(latitude, tplOriginal.latitude);
- Assert.Equal(longitude, tplOriginal.longitude);
- Assert.Equal(exifTagCount, tplOriginal.exifTagCount);
-
- var uploadToken = await _googlePhotosSvc.UploadMediaAsync(path, GooglePhotosUploadMethod.Simple);
- Assert.NotNull(uploadToken);
- var newMediaItemResult = await _googlePhotosSvc.AddMediaItemAsync(uploadToken, path);
- Assert.NotNull(newMediaItemResult);
- //the upload returns a null baseUrl
- Assert.Null(newMediaItemResult.mediaItem.baseUrl);
-
- //so now retrieve all media items
- var mediaItems = await _googlePhotosSvc.GetMediaItemsAsync().ToListAsync();
-
- var uploadedMediaItem = mediaItems.FirstOrDefault(p => p.filename.Equals(fileName));
- Assert.NotNull(uploadedMediaItem);
- Assert.True(uploadedMediaItem.isPhoto);
-
- var bytesNoExif = await _googlePhotosSvc.DownloadBytes(uploadedMediaItem, includeExifMetadata: false);
- Assert.NotNull(bytesNoExif);
- var tplNoExif = await ExifTests.GetExifInfo(bytesNoExif);
- Assert.True(googleExifTagCount == tplNoExif.exifTagCount);
-
- var bytesWithExif = await _googlePhotosSvc.DownloadBytes(uploadedMediaItem, includeExifMetadata: true);
- Assert.NotNull(bytesWithExif);
- var tplWithExif = await ExifTests.GetExifInfo(bytesWithExif);
- Assert.Null(tplWithExif.latitude);//location exif data always stripped :(
- Assert.Null(tplWithExif.longitude);//location exif data always stripped :(
- Assert.True(tplOriginal.exifTagCount > tplWithExif.exifTagCount);//due to Google-stripping fewer exif tags are returned
- Assert.True(googleExifTagCount < tplWithExif.exifTagCount);
- }
-
- static async Task<(double? latitude, double? longitude, int exifTagCount)> GetExifInfo(string path)
- {
- using var image = await Image.LoadAsync(path);
- return GetLatLong(image);
- }
-
- static async Task<(double? latitude, double? longitude, int exifTagCount)> GetExifInfo(byte[] bytes)
- {
- var stream = new MemoryStream(bytes);
- using var image = await Image.LoadAsync(stream);
- return GetLatLong(image);
- }
-
- static (double? latitude, double? longitude, int exifTagCount) GetLatLong(Image image)
- {
- double? latitude = null, longitude = null;
- var exifTagCount = image.Metadata.ExifProfile?.Values.Count ?? 0;
- if (image.Metadata.ExifProfile.Values?.Any() ?? false)
- {
- var exifData = image.Metadata.ExifProfile;
- if (exifData != null)
- {
- if (exifData.TryGetValue(ExifTag.GPSLatitude, out var gpsLatitude)
- && exifData.TryGetValue(ExifTag.GPSLatitudeRef, out var gpsLatitudeRef))
- latitude = GetCoordinates(gpsLatitudeRef.ToString(), gpsLatitude.Value);
-
- if (exifData.TryGetValue(ExifTag.GPSLongitude, out var gpsLong)
- && exifData.TryGetValue(ExifTag.GPSLongitudeRef, out var gpsLongRef))
- longitude = GetCoordinates(gpsLongRef.ToString(), gpsLong.Value);
-
- Debug.WriteLine($"latitude,longitude = {latitude},{longitude}");
- }
- }
-
- return (latitude, longitude, exifTagCount);
- }
-
- static double GetCoordinates(string gpsRef, Rational[] rationals)
- {
- var degrees = rationals[0].Numerator / rationals[0].Denominator;
- var minutes = rationals[1].Numerator / rationals[1].Denominator;
- var seconds = rationals[2].Numerator / rationals[2].Denominator;
-
- var coordinate = degrees + (minutes / 60d) + (seconds / 3600d);
- if (gpsRef == "S" || gpsRef == "W")
- coordinate *= -1;
- return coordinate;
- }
-}
diff --git a/src/CasCap.Apis.GooglePhotos/Exceptions/GooglePhotosException.cs b/src/CasCap.Apis.GooglePhotos/Exceptions/GooglePhotosException.cs
deleted file mode 100644
index 2c625ed..0000000
--- a/src/CasCap.Apis.GooglePhotos/Exceptions/GooglePhotosException.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-namespace CasCap.Exceptions;
-
-public class GooglePhotosException : Exception
-{
- public GooglePhotosException() { }
-
- public GooglePhotosException(Error error)
- : base(error is not null && error.error is not null && error.error.message is not null ? error.error.message : "unknown")
- {
- }
-}
diff --git a/src/CasCap.Apis.GooglePhotos/Extensions/DI.cs b/src/CasCap.Apis.GooglePhotos/Extensions/DI.cs
deleted file mode 100644
index bebfdc9..0000000
--- a/src/CasCap.Apis.GooglePhotos/Extensions/DI.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-using Polly;
-using System.Net;
-using System.Net.Http.Headers;
-namespace Microsoft.Extensions.DependencyInjection;
-
-public static class DI
-{
- public static void AddGooglePhotos(this IServiceCollection services)
- => services.AddGooglePhotos(_ => { });
-
- static readonly string sectionKey = $"{nameof(CasCap)}:{nameof(GooglePhotosOptions)}";
-
- public static void AddGooglePhotos(this IServiceCollection services, Action configure)
- {
- services.AddSingleton>(s =>
- {
- var configuration = s.GetService();
- return new ConfigureOptions(options => configuration?.Bind(sectionKey, options));
- });
- services.AddHttpClient((s, client) =>
- {
- var configuration = s.GetRequiredService();
- var options = configuration.GetSection(sectionKey).Get();
- options ??= new GooglePhotosOptions();//we use default BaseAddress if no config object injected in
- client.BaseAddress = new Uri(options.BaseAddress);
- client.DefaultRequestHeaders.Add("User-Agent", $"{nameof(CasCap)}.{AppDomain.CurrentDomain.FriendlyName}.{Environment.MachineName}");
- client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
- client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip"));
- client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("deflate"));
- client.Timeout = Timeout.InfiniteTimeSpan;
- }).ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
- {
- AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
- })
- .AddTransientHttpErrorPolicy(policyBuilder =>
- {
- return policyBuilder.RetryAsync(retryCount: 3);
- })
- //https://github.com/aspnet/AspNetCore/issues/6804
- .SetHandlerLifetime(Timeout.InfiniteTimeSpan);
- }
-}
diff --git a/src/CasCap.Apis.GooglePhotos/Models/PagingEventArgs.cs b/src/CasCap.Apis.GooglePhotos/Models/PagingEventArgs.cs
deleted file mode 100644
index 10228ea..0000000
--- a/src/CasCap.Apis.GooglePhotos/Models/PagingEventArgs.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-namespace CasCap.Models;
-
-public class PagingEventArgs : EventArgs
-{
- public PagingEventArgs(int pageSize, int pageNumber, int recordCount)
- {
- this.pageSize = pageSize;
- this.pageNumber = pageNumber;
- this.recordCount = recordCount;
- }
-
- public int pageSize { get; }
- public int pageNumber { get; }
- public int recordCount { get; }
- public DateTime? minDate { get; set; }
- public DateTime? maxDate { get; set; }
-}
diff --git a/src/CasCap.Apis.GooglePhotos/Models/UploadProgressEventArgs.cs b/src/CasCap.Apis.GooglePhotos/Models/UploadProgressEventArgs.cs
deleted file mode 100644
index dc6e323..0000000
--- a/src/CasCap.Apis.GooglePhotos/Models/UploadProgressEventArgs.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-namespace CasCap.Models;
-
-public class UploadProgressArgs : EventArgs
-{
- public UploadProgressArgs(string fileName, long totalBytes, int batchIndex, long uploadedBytes, long batchSize)
- {
- this.fileName = fileName;
- this.totalBytes = totalBytes;
- this.batchIndex = batchIndex;
- this.uploadedBytes = uploadedBytes;
- this.batchSize = batchSize;
- }
-
- public string fileName { get; }
- public long totalBytes { get; }
- public long batchIndex { get; }
- public long uploadedBytes { get; }
- public long batchSize { get; }
-}