cuepoint 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cuepoint-0.1.0/LICENSE +21 -0
- cuepoint-0.1.0/PKG-INFO +167 -0
- cuepoint-0.1.0/README.md +149 -0
- cuepoint-0.1.0/cuepoint/__init__.py +0 -0
- cuepoint-0.1.0/cuepoint/__main__.py +391 -0
- cuepoint-0.1.0/cuepoint/anlz.py +313 -0
- cuepoint-0.1.0/cuepoint/audio.py +138 -0
- cuepoint-0.1.0/cuepoint/extract.py +93 -0
- cuepoint-0.1.0/cuepoint/features.py +243 -0
- cuepoint-0.1.0/cuepoint/fingerprint.py +311 -0
- cuepoint-0.1.0/cuepoint/library.py +266 -0
- cuepoint-0.1.0/cuepoint/listen.py +316 -0
- cuepoint-0.1.0/cuepoint/mix.py +509 -0
- cuepoint-0.1.0/cuepoint/paths.py +66 -0
- cuepoint-0.1.0/cuepoint/server.py +136 -0
- cuepoint-0.1.0/cuepoint/static/fonts/OFL-Anton.txt +93 -0
- cuepoint-0.1.0/cuepoint/static/fonts/OFL-DMSans.txt +93 -0
- cuepoint-0.1.0/cuepoint/static/fonts/anton-latin-ext.woff2 +0 -0
- cuepoint-0.1.0/cuepoint/static/fonts/anton-latin.woff2 +0 -0
- cuepoint-0.1.0/cuepoint/static/fonts/dmsans-latin-ext.woff2 +0 -0
- cuepoint-0.1.0/cuepoint/static/fonts/dmsans-latin.woff2 +0 -0
- cuepoint-0.1.0/cuepoint/static/index.html +498 -0
- cuepoint-0.1.0/cuepoint.egg-info/PKG-INFO +167 -0
- cuepoint-0.1.0/cuepoint.egg-info/SOURCES.txt +28 -0
- cuepoint-0.1.0/cuepoint.egg-info/dependency_links.txt +1 -0
- cuepoint-0.1.0/cuepoint.egg-info/entry_points.txt +2 -0
- cuepoint-0.1.0/cuepoint.egg-info/requires.txt +2 -0
- cuepoint-0.1.0/cuepoint.egg-info/top_level.txt +1 -0
- cuepoint-0.1.0/pyproject.toml +39 -0
- cuepoint-0.1.0/setup.cfg +4 -0
cuepoint-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ena Otuokon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
cuepoint-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cuepoint
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: What to play next, and exactly where to bring it in. Reads a rekordbox collection, read-only.
|
|
5
|
+
Author-email: Ena Otuokon <otuokonhelena@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: dj,rekordbox,mixing,harmonic-mixing,music
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: numpy>=1.21
|
|
16
|
+
Requires-Dist: scipy>=1.7
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# Cuepoint
|
|
20
|
+
|
|
21
|
+
Cuepoint answers one question: **this record is playing — what comes next, and
|
|
22
|
+
where exactly do I bring it in?**
|
|
23
|
+
|
|
24
|
+
Every other tool stops at tempo and key. Two records can share both and still
|
|
25
|
+
wreck: the kicks land on different sixteenths, or both singers arrive at once.
|
|
26
|
+
Cuepoint listens to the parts that actually decide it.
|
|
27
|
+
|
|
28
|
+
It reads a local rekordbox collection strictly read-only. It never writes into
|
|
29
|
+
rekordbox's own files.
|
|
30
|
+
|
|
31
|
+
## Why it can do this
|
|
32
|
+
|
|
33
|
+
rekordbox has already analysed the collection — beat grid, downbeats, and the
|
|
34
|
+
phrase structure (intro / build / peak / breakdown / outro). That analysis sits
|
|
35
|
+
in the clear in `networkAnalyze6.db` and the `.DAT`/`.EXT` files beside it, so
|
|
36
|
+
none of it has to be recomputed. Cuepoint parses it, then decodes each track
|
|
37
|
+
once to measure what the analysis does not carry:
|
|
38
|
+
|
|
39
|
+
| feature | what it settles |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `groove_low` | where the kick sits in the bar — the clash you can't hear coming |
|
|
42
|
+
| `groove_full` | whether the overall feel matches, swung against straight |
|
|
43
|
+
| `chroma` | harmonic agreement, from the actual spectrum rather than a key label |
|
|
44
|
+
| `vocal` | is somebody singing here |
|
|
45
|
+
| `energy_db` | does the incoming record hold the level |
|
|
46
|
+
| `kick_hz` | do the two kick fundamentals beat against each other |
|
|
47
|
+
|
|
48
|
+
Grooves are measured against rekordbox's own grid and expressed as 24 slots of
|
|
49
|
+
one bar, so two records at different tempos compare directly.
|
|
50
|
+
|
|
51
|
+
## How a blend is scored
|
|
52
|
+
|
|
53
|
+
A recommendation is never just a track — it is a phrase of the outgoing record,
|
|
54
|
+
a phrase of the incoming one, and the number of bars they run together. Seven
|
|
55
|
+
components, each 0..1:
|
|
56
|
+
|
|
57
|
+
tempo 0.15 can the pitch fader reach it (also half and double time)
|
|
58
|
+
groove 0.22 do the two bars agree about where the weight falls
|
|
59
|
+
vocal 0.18 two vocals at once, the most audible mistake there is
|
|
60
|
+
harmonic 0.15 do the pitch classes agree
|
|
61
|
+
energy 0.12 does the level hold or lift
|
|
62
|
+
role 0.13 is this a sensible place to leave, and to arrive
|
|
63
|
+
sub 0.05 do the kick fundamentals fight in the bottom octave
|
|
64
|
+
|
|
65
|
+
Thresholds are measured, not guessed. Two phrases picked at random correlate at
|
|
66
|
+
0.14 in the low band while two phrases of the *same* record correlate at 0.81,
|
|
67
|
+
and the ramps are set across gaps like that one. Where a refinement failed to
|
|
68
|
+
earn its place it was removed and the reason left in the source — see
|
|
69
|
+
`harmonic_fit` in [`mix.py`](cuepoint/mix.py).
|
|
70
|
+
|
|
71
|
+
## Install
|
|
72
|
+
|
|
73
|
+
Requires **Python 3.9+**, **ffmpeg** on `PATH`, and a **rekordbox** collection
|
|
74
|
+
that has been analysed. numpy and scipy come in with the package.
|
|
75
|
+
|
|
76
|
+
pip install cuepoint # or: pipx install cuepoint
|
|
77
|
+
cuepoint doctor # checks ffmpeg, rekordbox, and where data lives
|
|
78
|
+
cuepoint setup # scan, measure, then open the UI
|
|
79
|
+
|
|
80
|
+
`setup` is the whole first run. It reads the rekordbox collection, decodes each
|
|
81
|
+
track once to measure what the analysis does not carry, and opens the web UI on
|
|
82
|
+
localhost:8765. Expect a few minutes for a few hundred tracks; the measurement
|
|
83
|
+
is cached per track, so later runs only touch what changed.
|
|
84
|
+
|
|
85
|
+
Everything Cuepoint derives lives in one folder, away from wherever you happen
|
|
86
|
+
to be standing:
|
|
87
|
+
|
|
88
|
+
macOS ~/Library/Application Support/Cuepoint
|
|
89
|
+
Windows %LOCALAPPDATA%\Cuepoint
|
|
90
|
+
Linux ~/.local/share/cuepoint
|
|
91
|
+
|
|
92
|
+
Set `CUEPOINT_HOME` to move it, or `CUEPOINT_REKORDBOX` if your collection is
|
|
93
|
+
not where rekordbox usually puts it. A directory that already contains a
|
|
94
|
+
`cuepoint.db` keeps using it, so nothing is stranded by upgrading.
|
|
95
|
+
|
|
96
|
+
## Use
|
|
97
|
+
|
|
98
|
+
After the first run, the individual steps are there when you need them:
|
|
99
|
+
|
|
100
|
+
cuepoint build # re-read rekordbox (after moving or adding music)
|
|
101
|
+
cuepoint extract # measure new tracks
|
|
102
|
+
cuepoint serve # the web UI
|
|
103
|
+
|
|
104
|
+
Run those three in that order whenever you move files or add music -- each one
|
|
105
|
+
feeds the next.
|
|
106
|
+
|
|
107
|
+
From the command line:
|
|
108
|
+
|
|
109
|
+
cuepoint tracks eleven # search
|
|
110
|
+
cuepoint show "Ten to Eleven" # structure, key, per-phrase detail
|
|
111
|
+
cuepoint next "Ten to Eleven" # what to play next
|
|
112
|
+
cuepoint set "Ten to Eleven" -n 10 # chain a whole set
|
|
113
|
+
|
|
114
|
+
`next` prints the mix points and the full score breakdown:
|
|
115
|
+
|
|
116
|
+
1. #########. 0.92 Find Your Corner
|
|
117
|
+
126.0 BPM C (8B)
|
|
118
|
+
out at 2:17 (peak) -> in at 0:30 (build) 4 bars pitch +0.0%
|
|
119
|
+
tempo 1.00 groove 1.00 vocal 0.98 harmonic 1.00 energy 0.99 ...
|
|
120
|
+
|
|
121
|
+
The web UI draws the same thing: both records on one time axis, aligned at the
|
|
122
|
+
bar where one becomes the other, plus a compatible-track filter -- pitch range
|
|
123
|
+
and Camelot key, the way a DJ browser works.
|
|
124
|
+
|
|
125
|
+
## Live mode
|
|
126
|
+
|
|
127
|
+
Not currently wired in. `fingerprint.py` and `listen.py` still build a
|
|
128
|
+
recognition index and follow the room, and `cuepoint index` / `cuepoint listen`
|
|
129
|
+
still run -- but the live capture path degrades chroma badly enough that the
|
|
130
|
+
matcher will not commit, so it is out of the UI until that is fixed. A track
|
|
131
|
+
matched from its file scores 0.998; the same audio captured live scores 0.94
|
|
132
|
+
with almost no margin over the runner-up.
|
|
133
|
+
|
|
134
|
+
## Layout
|
|
135
|
+
|
|
136
|
+
anlz.py parser for rekordbox's .DAT/.EXT analysis files
|
|
137
|
+
library.py builds cuepoint.db from the collection (read-only)
|
|
138
|
+
audio.py ffmpeg decode, spectrogram, onset envelope, grid alignment
|
|
139
|
+
features.py per-phrase groove / chroma / vocal / energy
|
|
140
|
+
extract.py runs features.py across the library, in parallel, cached
|
|
141
|
+
mix.py the scorer: what follows what, and where
|
|
142
|
+
fingerprint.py chroma index + closed-set recognition and playhead search
|
|
143
|
+
listen.py audio capture, hypothesis tracking, live state
|
|
144
|
+
server.py local web UI
|
|
145
|
+
paths.py where the database, features and index live, per platform
|
|
146
|
+
static/ the UI itself, and the two typefaces it sets (SIL OFL 1.1),
|
|
147
|
+
self-hosted so a booth with no wifi still looks right
|
|
148
|
+
|
|
149
|
+
Deliberately no librosa — everything used here is a few dozen lines of numpy.
|
|
150
|
+
|
|
151
|
+
## Caveats
|
|
152
|
+
|
|
153
|
+
`vocal` is a heuristic (harmonic/percussive separation plus a band-limited
|
|
154
|
+
energy ratio), not a trained detector. It separates "instrumental" from
|
|
155
|
+
"someone is singing", which is what the scorer needs; it will not catch a
|
|
156
|
+
heavily processed vocal chop.
|
|
157
|
+
|
|
158
|
+
Phrase roles come from rekordbox and inherit its mistakes — occasional two-bar
|
|
159
|
+
fragments are segmentation noise, and are skipped as mix points.
|
|
160
|
+
|
|
161
|
+
Live recognition assumes keylock is on, which is the modern default. With
|
|
162
|
+
keylock off, a large pitch move shifts energy across chroma bins and match
|
|
163
|
+
confidence will fall.
|
|
164
|
+
|
|
165
|
+
Streaming catalogues (Beatport, Beatsource, TIDAL) are not integrated. The
|
|
166
|
+
Beatport v4 API is OAuth behind an approved-partner portal and in-app streaming
|
|
167
|
+
needs a commercial LINK agreement, so it is not reachable without a deal.
|
cuepoint-0.1.0/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Cuepoint
|
|
2
|
+
|
|
3
|
+
Cuepoint answers one question: **this record is playing — what comes next, and
|
|
4
|
+
where exactly do I bring it in?**
|
|
5
|
+
|
|
6
|
+
Every other tool stops at tempo and key. Two records can share both and still
|
|
7
|
+
wreck: the kicks land on different sixteenths, or both singers arrive at once.
|
|
8
|
+
Cuepoint listens to the parts that actually decide it.
|
|
9
|
+
|
|
10
|
+
It reads a local rekordbox collection strictly read-only. It never writes into
|
|
11
|
+
rekordbox's own files.
|
|
12
|
+
|
|
13
|
+
## Why it can do this
|
|
14
|
+
|
|
15
|
+
rekordbox has already analysed the collection — beat grid, downbeats, and the
|
|
16
|
+
phrase structure (intro / build / peak / breakdown / outro). That analysis sits
|
|
17
|
+
in the clear in `networkAnalyze6.db` and the `.DAT`/`.EXT` files beside it, so
|
|
18
|
+
none of it has to be recomputed. Cuepoint parses it, then decodes each track
|
|
19
|
+
once to measure what the analysis does not carry:
|
|
20
|
+
|
|
21
|
+
| feature | what it settles |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `groove_low` | where the kick sits in the bar — the clash you can't hear coming |
|
|
24
|
+
| `groove_full` | whether the overall feel matches, swung against straight |
|
|
25
|
+
| `chroma` | harmonic agreement, from the actual spectrum rather than a key label |
|
|
26
|
+
| `vocal` | is somebody singing here |
|
|
27
|
+
| `energy_db` | does the incoming record hold the level |
|
|
28
|
+
| `kick_hz` | do the two kick fundamentals beat against each other |
|
|
29
|
+
|
|
30
|
+
Grooves are measured against rekordbox's own grid and expressed as 24 slots of
|
|
31
|
+
one bar, so two records at different tempos compare directly.
|
|
32
|
+
|
|
33
|
+
## How a blend is scored
|
|
34
|
+
|
|
35
|
+
A recommendation is never just a track — it is a phrase of the outgoing record,
|
|
36
|
+
a phrase of the incoming one, and the number of bars they run together. Seven
|
|
37
|
+
components, each 0..1:
|
|
38
|
+
|
|
39
|
+
tempo 0.15 can the pitch fader reach it (also half and double time)
|
|
40
|
+
groove 0.22 do the two bars agree about where the weight falls
|
|
41
|
+
vocal 0.18 two vocals at once, the most audible mistake there is
|
|
42
|
+
harmonic 0.15 do the pitch classes agree
|
|
43
|
+
energy 0.12 does the level hold or lift
|
|
44
|
+
role 0.13 is this a sensible place to leave, and to arrive
|
|
45
|
+
sub 0.05 do the kick fundamentals fight in the bottom octave
|
|
46
|
+
|
|
47
|
+
Thresholds are measured, not guessed. Two phrases picked at random correlate at
|
|
48
|
+
0.14 in the low band while two phrases of the *same* record correlate at 0.81,
|
|
49
|
+
and the ramps are set across gaps like that one. Where a refinement failed to
|
|
50
|
+
earn its place it was removed and the reason left in the source — see
|
|
51
|
+
`harmonic_fit` in [`mix.py`](cuepoint/mix.py).
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
Requires **Python 3.9+**, **ffmpeg** on `PATH`, and a **rekordbox** collection
|
|
56
|
+
that has been analysed. numpy and scipy come in with the package.
|
|
57
|
+
|
|
58
|
+
pip install cuepoint # or: pipx install cuepoint
|
|
59
|
+
cuepoint doctor # checks ffmpeg, rekordbox, and where data lives
|
|
60
|
+
cuepoint setup # scan, measure, then open the UI
|
|
61
|
+
|
|
62
|
+
`setup` is the whole first run. It reads the rekordbox collection, decodes each
|
|
63
|
+
track once to measure what the analysis does not carry, and opens the web UI on
|
|
64
|
+
localhost:8765. Expect a few minutes for a few hundred tracks; the measurement
|
|
65
|
+
is cached per track, so later runs only touch what changed.
|
|
66
|
+
|
|
67
|
+
Everything Cuepoint derives lives in one folder, away from wherever you happen
|
|
68
|
+
to be standing:
|
|
69
|
+
|
|
70
|
+
macOS ~/Library/Application Support/Cuepoint
|
|
71
|
+
Windows %LOCALAPPDATA%\Cuepoint
|
|
72
|
+
Linux ~/.local/share/cuepoint
|
|
73
|
+
|
|
74
|
+
Set `CUEPOINT_HOME` to move it, or `CUEPOINT_REKORDBOX` if your collection is
|
|
75
|
+
not where rekordbox usually puts it. A directory that already contains a
|
|
76
|
+
`cuepoint.db` keeps using it, so nothing is stranded by upgrading.
|
|
77
|
+
|
|
78
|
+
## Use
|
|
79
|
+
|
|
80
|
+
After the first run, the individual steps are there when you need them:
|
|
81
|
+
|
|
82
|
+
cuepoint build # re-read rekordbox (after moving or adding music)
|
|
83
|
+
cuepoint extract # measure new tracks
|
|
84
|
+
cuepoint serve # the web UI
|
|
85
|
+
|
|
86
|
+
Run those three in that order whenever you move files or add music -- each one
|
|
87
|
+
feeds the next.
|
|
88
|
+
|
|
89
|
+
From the command line:
|
|
90
|
+
|
|
91
|
+
cuepoint tracks eleven # search
|
|
92
|
+
cuepoint show "Ten to Eleven" # structure, key, per-phrase detail
|
|
93
|
+
cuepoint next "Ten to Eleven" # what to play next
|
|
94
|
+
cuepoint set "Ten to Eleven" -n 10 # chain a whole set
|
|
95
|
+
|
|
96
|
+
`next` prints the mix points and the full score breakdown:
|
|
97
|
+
|
|
98
|
+
1. #########. 0.92 Find Your Corner
|
|
99
|
+
126.0 BPM C (8B)
|
|
100
|
+
out at 2:17 (peak) -> in at 0:30 (build) 4 bars pitch +0.0%
|
|
101
|
+
tempo 1.00 groove 1.00 vocal 0.98 harmonic 1.00 energy 0.99 ...
|
|
102
|
+
|
|
103
|
+
The web UI draws the same thing: both records on one time axis, aligned at the
|
|
104
|
+
bar where one becomes the other, plus a compatible-track filter -- pitch range
|
|
105
|
+
and Camelot key, the way a DJ browser works.
|
|
106
|
+
|
|
107
|
+
## Live mode
|
|
108
|
+
|
|
109
|
+
Not currently wired in. `fingerprint.py` and `listen.py` still build a
|
|
110
|
+
recognition index and follow the room, and `cuepoint index` / `cuepoint listen`
|
|
111
|
+
still run -- but the live capture path degrades chroma badly enough that the
|
|
112
|
+
matcher will not commit, so it is out of the UI until that is fixed. A track
|
|
113
|
+
matched from its file scores 0.998; the same audio captured live scores 0.94
|
|
114
|
+
with almost no margin over the runner-up.
|
|
115
|
+
|
|
116
|
+
## Layout
|
|
117
|
+
|
|
118
|
+
anlz.py parser for rekordbox's .DAT/.EXT analysis files
|
|
119
|
+
library.py builds cuepoint.db from the collection (read-only)
|
|
120
|
+
audio.py ffmpeg decode, spectrogram, onset envelope, grid alignment
|
|
121
|
+
features.py per-phrase groove / chroma / vocal / energy
|
|
122
|
+
extract.py runs features.py across the library, in parallel, cached
|
|
123
|
+
mix.py the scorer: what follows what, and where
|
|
124
|
+
fingerprint.py chroma index + closed-set recognition and playhead search
|
|
125
|
+
listen.py audio capture, hypothesis tracking, live state
|
|
126
|
+
server.py local web UI
|
|
127
|
+
paths.py where the database, features and index live, per platform
|
|
128
|
+
static/ the UI itself, and the two typefaces it sets (SIL OFL 1.1),
|
|
129
|
+
self-hosted so a booth with no wifi still looks right
|
|
130
|
+
|
|
131
|
+
Deliberately no librosa — everything used here is a few dozen lines of numpy.
|
|
132
|
+
|
|
133
|
+
## Caveats
|
|
134
|
+
|
|
135
|
+
`vocal` is a heuristic (harmonic/percussive separation plus a band-limited
|
|
136
|
+
energy ratio), not a trained detector. It separates "instrumental" from
|
|
137
|
+
"someone is singing", which is what the scorer needs; it will not catch a
|
|
138
|
+
heavily processed vocal chop.
|
|
139
|
+
|
|
140
|
+
Phrase roles come from rekordbox and inherit its mistakes — occasional two-bar
|
|
141
|
+
fragments are segmentation noise, and are skipped as mix points.
|
|
142
|
+
|
|
143
|
+
Live recognition assumes keylock is on, which is the modern default. With
|
|
144
|
+
keylock off, a large pitch move shifts energy across chroma bins and match
|
|
145
|
+
confidence will fall.
|
|
146
|
+
|
|
147
|
+
Streaming catalogues (Beatport, Beatsource, TIDAL) are not integrated. The
|
|
148
|
+
Beatport v4 API is OAuth behind an approved-partner portal and in-app streaming
|
|
149
|
+
needs a commercial LINK agreement, so it is not reachable without a deal.
|
|
File without changes
|