Skip to content

Commit 6b0c69d

Browse files
committed
0.3.0
1 parent 57eab7f commit 6b0c69d

26 files changed

Lines changed: 1416 additions & 117 deletions

.env.dist

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Copy to .env and fill in to enable the live test suite: composer tests:live
2+
# Register an application at https://www.discogs.com/settings/developers to get these.
3+
# .env is gitignored - never commit real credentials.
4+
DISCOGS_API_KEY=
5+
DISCOGS_API_SECRET=
6+
7+
# Optional. A Discogs username with a public collection and wantlist, used by the
8+
# user-endpoint tests. Those are skipped when this is empty.
9+
DISCOGS_TEST_USERNAME=

.gitattributes

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,14 @@
11
/tests export-ignore
22
/examples export-ignore
3+
/resources export-ignore
4+
/.github export-ignore
5+
/.claude export-ignore
6+
/.gitattributes export-ignore
7+
/.gitignore export-ignore
8+
/.php-cs-fixer.php export-ignore
9+
/phpstan.neon export-ignore
10+
/phpunit.xml export-ignore
11+
/rector.php export-ignore
12+
/.env.dist export-ignore
13+
/.env export-ignore
14+
/.env.local export-ignore

.github/workflows/qa.yml

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ jobs:
1010
build:
1111
runs-on: ${{ matrix.os }}
1212
strategy:
13+
fail-fast: false
1314
matrix:
1415
os: ['ubuntu-latest']
15-
php: ['8.2', '8.3']
16-
continue-on-error: ${{ matrix.php == '8.3' }}
16+
# 8.2 is the composer.json floor, 8.5 is what Ampache develop requires of its consumers
17+
php: ['8.2', '8.3', '8.4', '8.5']
1718
steps:
1819
- uses: actions/checkout@v4
1920

@@ -22,27 +23,18 @@ jobs:
2223
with:
2324
php-version: ${{ matrix.php }}
2425

25-
- name: Validate composer.json and composer.lock
26+
- name: Validate composer.json
2627
run: composer validate
2728

2829
- name: Install dependencies
29-
if: ${{ matrix.php != '8.3' }}
3030
uses: nick-fields/retry@v3
3131
with:
3232
timeout_minutes: 5
3333
max_attempts: 3
3434
command: composer update --no-interaction --no-progress
3535

36-
- name: Install Dependencies (ignore platform)
37-
if: ${{ matrix.php == '8.3' }}
38-
uses: nick-fields/retry@v3
39-
with:
40-
timeout_minutes: 5
41-
max_attempts: 3
42-
command: composer update --no-interaction --no-progress --ignore-platform-req=php
43-
4436
- name: Run test suite
4537
run: composer run-script qa
46-
env:
47-
PHP_CS_FIXER_IGNORE_ENV: 1
4838

39+
- name: Run static analysis
40+
run: composer run-script stan

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@
44
composer.lock
55
.php-cs-fixer.cache
66
.phpunit.result.cache
7+
.env
8+
.env.local

CHANGELOG.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,65 @@
11
# AmpacheDiscogs
22

3+
## 0.3.0
4+
5+
Requests are now paced to the documented Discogs rate limit and retried when Discogs pushes back.
6+
7+
**NOTE** Sustained lookups are slower than 0.2.0 by design. The old fixed 0.5s pause allowed about
8+
120 requests a minute against a 60 a minute limit, so a long run earned a 429 and gave up.
9+
10+
### Added (0.3.0)
11+
12+
* A trailing `int $page = 1` on every endpoint Discogs paginates, so results past the first page
13+
are reachable at all — `get_artist_releases`, `get_label_releases`, `get_master_versions`,
14+
`get_collection_items_by_folder`, `get_user_lists`, `get_wantlist`, `search_album`,
15+
`search_artist`, `search_master` and `search_release`
16+
* previously only page 1 was ever requested, so `get_label_releases(1)` reached 50 of 592
17+
releases and `search_album()` reached 10 of 487 matches, with no way to ask for the rest
18+
* the `pagination` block Discogs returns tells you how many pages there are
19+
* **NOTE** `get_artist_releases()` repeats a few records across page boundaries and returns 49
20+
rows for a page size of 50. That is Discogs itself, on the raw endpoint, under every sort
21+
option — collect its pages keyed on `id`. The other paged endpoints do not do this
22+
* a page below 1 is clamped to 1 rather than sent, since Discogs rejects it
23+
* endpoints that return a single object are unchanged: `get_list`, `get_profile`,
24+
`get_collection_folders` and the individual artist, label, master and release lookups
25+
* `DiscogsException`, thrown in place of `Exception`, carrying the HTTP status
26+
* `getStatusCode()` tells a missing record (404) apart from a rate limit (429) or an outage (5xx)
27+
* `isRateLimited()` for the 429 case
28+
* extends `Exception`, so an existing `catch (Exception $error)` still works
29+
* Automatic retry for `429`, `500`, `502`, `503` and `504`, up to three attempts per call
30+
* honours the `Retry-After` header when Discogs sends one
31+
* `X-Discogs-Ratelimit-Remaining` widens the gap between requests as the window is used up
32+
33+
### Changed (0.3.0)
34+
35+
* Requests are spaced by elapsed time rather than a fixed pause after each one, so a slow response
36+
no longer adds its duration to the next wait
37+
* Usernames are URL encoded, so one containing `/`, `?`, `#` or `&` reaches the intended endpoint
38+
instead of altering the request
39+
* Tested against PHP 8.2, 8.3, 8.4 and 8.5, all of them gated
40+
* 8.5 is the version Ampache 8 runs, and nothing previously covered it
41+
* Only `src/`, the docs and `composer.json` are published now
42+
* `phpstan.neon`, `rector.php`, `phpunit.xml`, `.php-cs-fixer.php` and `.github/` are marked
43+
`export-ignore`, so they no longer land in a consumer's `vendor/` directory
44+
45+
### Fixed (0.3.0)
46+
47+
* **The declared license was wrong.** `composer.json` has said `MIT` since the initial commit,
48+
contradicting the AGPL-3.0 text in `LICENSE.md` and the AGPL-3.0-or-later header on every source
49+
file. It now reads `AGPL-3.0-or-later`, which is what this library has always been under — it is
50+
an export of the AGPL-licensed Ampache Discogs plugin, so MIT was never a license it could offer.
51+
* this corrects the metadata to match the license that already applied; it does not relicense
52+
anything and does not change what you may do with the code
53+
* anyone who relied on the `MIT` declaration — an automated license audit, a dependency policy
54+
check — was given the wrong answer and should re-run it
55+
* the `-or-later` suffix matches the header wording; a bare `AGPL-3.0` is a deprecated SPDX
56+
identifier that `composer validate --strict` rejects
57+
* `search_release()` returned masters mixed in with the releases
58+
* it sent `type=releases`, which Discogs does not recognise, and an unknown type is ignored rather
59+
than rejected, so the search came back unfiltered
60+
* The scripts in `examples/` stopped before their first request, passing `null` where the
61+
constructor requires a string
62+
363
## 0.2.0
464

565
Missing functions and examples have been added.

README.md

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,82 @@ The focus here is on keeping it small and simple.
66

77
All data is JSON decoded with objects converted into associative arrays.
88

9+
## License
10+
11+
AGPL-3.0-or-later. See [LICENSE.md](LICENSE.md).
12+
13+
This library is an export of the Ampache Discogs plugin and carries Ampache's license. Releases
14+
before 0.3.0 declared `MIT` in `composer.json`; that was a mistake in the metadata, not a different
15+
license — the AGPL text and the source headers were there from the initial commit.
16+
917
## Requirements
1018

1119
* PHP8.2+
1220
* rmccue/requests
1321

22+
## Paging
23+
24+
Discogs paginates the list endpoints. Every method that hits one takes a trailing `int $page = 1`,
25+
and the `pagination` block in the response says how many pages there are:
26+
27+
```php
28+
$page = $discogs->get_label_releases(1);
29+
$total = $page['pagination']['pages'];
30+
31+
for ($number = 1; $number <= $total; $number++) {
32+
$releases = $discogs->get_label_releases(1, $number)['releases'];
33+
// ...
34+
}
35+
```
36+
37+
Paged: `get_artist_releases`, `get_label_releases`, `get_master_versions`,
38+
`get_collection_items_by_folder`, `get_user_lists`, `get_wantlist`, `search_album`,
39+
`search_artist`, `search_master`, `search_release`. Pass a page to `search()` in its parameter
40+
array. Anything returning a single record takes no page.
41+
42+
Each request is paced to about a second, so walking a long list is not instant.
43+
44+
**`get_artist_releases()` repeats a few records across page boundaries** and returns 49 rows for a
45+
`per_page` of 50. That is Discogs, not this library: it happens on the raw endpoint under every
46+
`sort` option. Key on `id` when collecting pages from it:
47+
48+
```php
49+
$byId = [];
50+
for ($number = 1; $number <= $total; $number++) {
51+
foreach ($discogs->get_artist_releases($artistId, $number)['releases'] as $release) {
52+
$byId[$release['id']] = $release;
53+
}
54+
}
55+
```
56+
57+
The other paged endpoints — label releases, master versions, wantlists and search — return
58+
non-overlapping pages.
59+
60+
## Rate limiting and errors
61+
62+
Discogs allows 60 authenticated requests a minute. The client paces itself to that, widens the gap
63+
as `X-Discogs-Ratelimit-Remaining` runs down, and retries a `429` or a `5xx` up to three times,
64+
honouring `Retry-After`. Nothing is required of the caller for that to happen.
65+
66+
Anything that still fails throws `AmpacheDiscogs\DiscogsException`, which extends `Exception` and
67+
carries the HTTP status so the cases can be told apart:
68+
69+
```php
70+
use AmpacheDiscogs\DiscogsException;
71+
72+
try {
73+
$album = $discogs->get_master(1234);
74+
} catch (DiscogsException $error) {
75+
if ($error->getStatusCode() === 404) {
76+
// no such record, nothing to retry
77+
} elseif ($error->isRateLimited()) {
78+
// still limited after three attempts, come back later
79+
}
80+
81+
print_r($error->getMessage());
82+
}
83+
```
84+
1485
## Usage Example
1586

1687
```php
@@ -29,10 +100,10 @@ $media = [
29100

30101
echo "Checking: " . print_r($media, true) . PHP_EOL;
31102
try {
32-
// your own username and password are required to use the Discogs API
33-
$username = 'username';
34-
$password = 'password';
35-
$discogs = new Discogs($username, $password);
103+
// your own Discogs api key and secret are required to use the Discogs API
104+
$api_key = 'yourApiKey';
105+
$secret = 'yourApiSecret';
106+
$discogs = new Discogs($api_key, $secret);
36107

37108
/**
38109
* https://api.discogs.com/database/search?type=master&release_title=The+Shape&artist=Code+64&per_page=10&key=key@secret=secret
@@ -118,3 +189,24 @@ try {
118189
```
119190

120191
Look in the [/examples](https://github.com/ampache/php-discogs-api/tree/master/examples) folder for more.
192+
193+
## Testing
194+
195+
```shell
196+
composer qa # syntax, code style and the offline unit tests
197+
composer stan # static analysis
198+
composer tests:live # really calls Discogs, needs credentials
199+
```
200+
201+
The unit suite never touches the network. The live suite is opt-in: copy `.env.dist` to `.env` and
202+
fill in a key and secret from <https://www.discogs.com/settings/developers>. Without them the live
203+
tests skip, so `composer qa` and CI stay green on a fresh checkout. `.env` is gitignored.
204+
205+
```ini
206+
DISCOGS_API_KEY=yourApiKey
207+
DISCOGS_API_SECRET=yourApiSecret
208+
DISCOGS_TEST_USERNAME=someUserWithAPublicCollection
209+
```
210+
211+
`DISCOGS_TEST_USERNAME` is optional and only gates the user endpoint tests. Environment variables
212+
take precedence over the file, so CI can supply the same names as secrets.

composer.json

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "A PHP library for accessing the Discogs API",
55
"keywords": ["discogs", "library", "php", "api"],
66
"homepage": "https://github.com/ampache/php-discogs-api",
7-
"license": "MIT",
7+
"license": "AGPL-3.0-or-later",
88
"authors": [
99
{
1010
"name": "Lachlan de Waard",
@@ -19,15 +19,18 @@
1919
},
2020
"require-dev": {
2121
"friendsofphp/php-cs-fixer": "^3.49",
22-
"phpstan/phpstan": "^1.10",
22+
"phpstan/phpstan": "^2",
2323
"phpunit/phpunit": "^11",
24-
"rector/rector": "^1"
24+
"rector/rector": "^2"
2525
},
2626
"scripts": {
27-
"qa": "composer run-script cs:check",
27+
"coverage": "XDEBUG_MODE=coverage ./vendor/bin/phpunit -c phpunit.xml --testsuite default --coverage-html build/coverage",
28+
"qa": "composer run-script syntax && composer run-script cs:check && composer run-script tests",
2829
"stan": "vendor/bin/phpstan analyse",
2930
"stan-baseline": "vendor/bin/phpstan --generate-baseline",
30-
"tests": "vendor/bin/phpunit -c phpunit.xml",
31+
"tests": "vendor/bin/phpunit -c phpunit.xml --testsuite default",
32+
"tests:live": "vendor/bin/phpunit -c phpunit.xml --testsuite live",
33+
"syntax": "@php resources/scripts/tests/syntax.php",
3134
"cs:fix": "vendor/bin/php-cs-fixer fix",
3235
"cs:check": "vendor/bin/php-cs-fixer fix --dry-run -vv",
3336
"rector:dry": "rector process -n",
@@ -38,18 +41,23 @@
3841
"qa": "Runs several qa-related tests",
3942
"stan": "Performs static analysis",
4043
"stan-baseline": "Regenerate phpstan baseline",
41-
"tests": "Executes the unit tests",
44+
"tests": "Executes the offline unit tests",
45+
"tests:live": "Executes the live tests against the real Discogs API (needs .env)",
4246
"syntax": "Performs php syntax checks",
4347
"cs:fix": "Performs code-style corrections on the whole codebase",
4448
"cs:check": "Performs a code-style dry-run on the whole codebase",
4549
"rector:dry": "Performs rector code-migrations dry-run",
4650
"rector:fix": "Applies pending rector code-migrations"
4751
},
48-
"suggest": {
49-
},
5052
"autoload": {
5153
"psr-0": {
5254
"AmpacheDiscogs": "src/"
5355
}
56+
},
57+
"autoload-dev": {
58+
"psr-0": {
59+
"AmpacheDiscogs\\Tests": "tests/",
60+
"AmpacheDiscogs\\Live": "tests/"
61+
}
5462
}
5563
}

examples/collections.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44

55
require dirname(__DIR__) . '/vendor/autoload.php';
66

7-
// your own username and password are required to use the Discogs API
8-
$username = null;
9-
$password = null;
10-
$discogs = new Discogs($username, $password);
7+
// your own Discogs api key and secret are required to use the Discogs API
8+
$api_key = 'yourApiKey';
9+
$secret = 'yourApiSecret';
10+
$discogs = new Discogs($api_key, $secret);
1111
$username = 'discogsUsername';
1212

1313
try {

examples/labels.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44

55
require dirname(__DIR__) . '/vendor/autoload.php';
66

7-
// your own username and password are required to use the Discogs API
8-
$username = null;
9-
$password = null;
10-
$discogs = new Discogs($username, $password);
7+
// your own Discogs api key and secret are required to use the Discogs API
8+
$api_key = 'yourApiKey';
9+
$secret = 'yourApiSecret';
10+
$discogs = new Discogs($api_key, $secret);
1111
$label_id = 1212668;
1212

1313
try {

examples/releases.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44

55
require dirname(__DIR__) . '/vendor/autoload.php';
66

7-
// your own username and password are required to use the Discogs API
8-
$username = null;
9-
$password = null;
10-
$discogs = new Discogs($username, $password);
7+
// your own Discogs api key and secret are required to use the Discogs API
8+
$api_key = 'yourApiKey';
9+
$secret = 'yourApiSecret';
10+
$discogs = new Discogs($api_key, $secret);
1111

1212
$artist = 'Code 64';
1313
$album = 'The Shape';

0 commit comments

Comments
 (0)