├── .dockerignore
├── .editorconfig
├── .env.example
├── .gitignore
├── .travis.yml
├── Dockerfile
├── README.md
├── bin
├── dev_setup.sh
├── docker_build.sh
├── docker_push.sh
├── flickr-cli
└── phpstan.sh
├── composer.json
├── composer.lock
├── flickr-cli.sublime-project
├── phpcs.xml
└── src
└── TheFox
├── FlickrCli
├── Command
│ ├── AlbumsCommand.php
│ ├── AuthCommand.php
│ ├── DeleteCommand.php
│ ├── DownloadCommand.php
│ ├── FilesCommand.php
│ ├── FlickrCliCommand.php
│ ├── PiwigoCommand.php
│ └── UploadCommand.php
├── Exception
│ └── SignalException.php
├── FlickrCli.php
└── Service
│ ├── AbstractService.php
│ └── ApiService.php
└── OAuth
├── Common
└── Http
│ └── Client
│ └── GuzzleStreamClient.php
└── OAuth1
└── Service
└── Flickr.php
/.dockerignore:
--------------------------------------------------------------------------------
1 | /log
2 | /vendor
3 | /tmp
4 | *.txt
5 | *.yml
6 | *.sublime-project
7 | *.sublime-workspace
8 | .DS_Store
9 | .editorconfig
10 | .git
11 | .gitignore
12 | .idea
13 | .env
14 | .env.example
15 | README.md
16 | phpcs.xml
17 |
18 | .dockerignore
19 | Dockerfile
20 |
21 | *.lock1
22 | Dockerfile1
23 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | indent_style = space
7 | indent_size = 4
8 | end_of_line = lf
9 | charset = utf-8
10 | trim_trailing_whitespace = false
11 | insert_final_newline = true
12 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | IMAGE_NAME="thefox21/flickr-cli"
2 | GITHUB_API_TOKEN=""
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | CHANGELOG-*.txt
2 | config.yml
3 | composer.phar
4 | /vendor/
5 | /tmp/
6 | /log/
7 | .env
8 | .idea
9 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 | php:
3 | - 7.0
4 | - 7.1
5 | - 7.2
6 | install:
7 | - composer install --prefer-source --no-interaction
8 | before_script:
9 | - phpenv rehash
10 | script:
11 | - ./bin/phpstan.sh
12 | - ./vendor/bin/phpcs --config-set ignore_warnings_on_exit 1
13 | - ./vendor/bin/phpcs --config-show
14 | - ./vendor/bin/phpcs
15 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM php:7.2-rc-cli
2 | ARG COMPOSER_AUTH
3 |
4 | ENV DEBIAN_FRONTEND noninteractive
5 | ENV FLICKRCLI_CONFIG /data/config.yml
6 |
7 | RUN apt-get update && \
8 | apt-get install -y apt-transport-https build-essential curl libcurl3 libcurl4-openssl-dev libicu-dev zlib1g-dev libxml2-dev && \
9 | docker-php-ext-install curl xml zip bcmath pcntl && \
10 | apt-get clean
11 |
12 | # Install Composer.
13 | COPY --from=composer:1.5 /usr/bin/composer /usr/bin/composer
14 |
15 | # Root App folder
16 | RUN mkdir /app
17 | WORKDIR /app
18 | ADD . /app
19 |
20 | # Install dependencies.
21 | RUN composer install --no-dev --optimize-autoloader --no-progress --no-suggest --no-interaction
22 |
23 | RUN ls -la
24 |
25 | RUN rm -r /root/.composer/* /root/.composer
26 | RUN ls -la /root
27 |
28 | # Use to store the config inside a volume.
29 | RUN mkdir /data && chmod 777 /data
30 | VOLUME /data
31 |
32 | VOLUME /mnt
33 | WORKDIR /mnt
34 |
35 | ENTRYPOINT ["php", "/app/bin/flickr-cli"]
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FlickrCLI
2 |
3 | A command-line interface to [Flickr](https://www.flickr.com/). Upload and download photos, photo sets, directories via shell.
4 |
5 | ## Installation
6 |
7 | 1. Clone from Github:
8 |
9 | git clone https://github.com/TheFox/flickr-cli.git
10 |
11 | 2. Install dependencies:
12 |
13 | composer install
14 |
15 | 3. Go to to create a new API key.
16 | The first time you run `./bin/flickr-cli auth` you'll be prompted to enter your new consumer key and secret.
17 |
18 | ## Usage
19 |
20 | First, get the access token:
21 |
22 | ./bin/flickr-cli auth
23 |
24 | ### Upload
25 |
26 | ./bin/flickr-cli upload [-d DESCRIPTION] [-t TAG,...] [-s SET,...] DIRECTORY...
27 |
28 | ### Download
29 |
30 | ./bin/flickr-cli download -d DIRECTORY [SET...]
31 |
32 | To download all photosets to directory `photosets`:
33 |
34 | ./bin/flickr-cli download -d photosets
35 |
36 | Or to download only the photoset *Holiday 2013*:
37 |
38 | ./bin/flickr-cli download -d photosets 'Holiday 2013'
39 |
40 | To download all photos into directories named by photo ID
41 | (and so which will not change when you rename albums or photos; perfect for a complete Flickr backup)
42 | you can use the `--id-dirs` option:
43 |
44 | ./bin/flickr-cli download -d flickr_backup --id-dirs
45 |
46 | This creates a stable directory structure of the form `destination_dir/hash/hash/photo-ID/`
47 | and saves the full original photo file along with a `metadata.yml` file containing all photo metadata.
48 | The hashes, which are the first two sets of two characters of the MD5 hash of the ID,
49 | are required in order to prevent a single directory from containing too many subdirectories
50 | (to avoid problems with some filesystems).
51 |
52 | ## Usage of the Docker Image
53 |
54 | ### Setup
55 |
56 | To use this software within Docker follow this steps.
57 |
58 | 1. Create a volume. This is used to store the configuration file for the `auth` step.
59 |
60 | docker volume create flickrcli
61 |
62 | 2. Get the access token (it will create `config.yml` file in the volume).
63 |
64 | docker run --rm -it -u $(id -u):$(id -g) -v "$PWD":/mnt -v flickrcli:/data thefox21/flickr-cli auth
65 |
66 | or you can store the `config.yml` in your `$HOME/.flickr-cli` directory and use:
67 |
68 | mkdir $HOME/.flickr-cli
69 | docker run --rm -it -u $(id -u):$(id -g) -v "$PWD":/mnt -v "$HOME/.flickr-cli":/data thefox21/flickr-cli auth
70 |
71 | ### Usage
72 |
73 | Upload directory `2017.06.01-Spindleruv_mlyn` full of JPEGs to Flickr:
74 |
75 | docker run --rm -it -u $(id -u):$(id -g) -v "$PWD":/mnt -v flickrcli:/data thefox21/flickr-cli upload --config=/data/config.yml --tags "2017.06.01 Spindleruv_mlyn" --sets "2017.06.01-Spindleruv_mlyn" 2017.06.01-Spindleruv_mlyn
76 |
77 | For Docker image troubleshooting you can use:
78 |
79 | docker run --rm -it -u $(id -u):$(id -g) -v "$PWD":/mnt -v flickrcli:/data --entrypoint=/bin/bash thefox21/flickr-cli
80 |
81 | ### Paths
82 |
83 | - `/app` - Main Application directory.
84 | - `/data` - Volume for variable data.
85 | - `/mnt` - Host system's `$PWD`.
86 |
87 | ## Documentations
88 |
89 | - [Flickr API documentation](http://www.flickr.com/services/api/)
90 | - [Docker documentation](https://docs.docker.com/)
91 |
92 | ## License
93 |
94 | Copyright (C) 2016 Christian Mayer
95 |
96 | This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
97 |
98 | This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see .
99 |
--------------------------------------------------------------------------------
/bin/dev_setup.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | SCRIPT_BASEDIR=$(dirname "$0")
4 |
5 |
6 | set -e
7 | cd "${SCRIPT_BASEDIR}/.."
8 |
9 | which php &> /dev/null || { echo 'ERROR: php not found in PATH'; exit 1; }
10 | which composer &> /dev/null || { echo 'ERROR: composer not found in PATH'; exit 1; }
11 |
12 | if [[ ! -f .env ]]; then
13 | cp .env.example .env
14 | fi
15 |
16 | composer install --no-interaction
17 |
--------------------------------------------------------------------------------
/bin/docker_build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Builds the Docker images.
4 |
5 | DATE=$(date +"%Y%m%d_%H%M%S")
6 | SCRIPT_BASEDIR=$(dirname "$0")
7 |
8 |
9 | set -e
10 | which docker &> /dev/null || { echo 'ERROR: docker not found in PATH'; exit 1; }
11 | which sed &> /dev/null || { echo 'ERROR: sed not found in PATH'; exit 1; }
12 |
13 | cd "${SCRIPT_BASEDIR}/.."
14 | source ./.env
15 |
16 | docker build --tag ${IMAGE_NAME}:${DATE} --build-arg COMPOSER_AUTH="{\"github.com\":\"$GITHUB_API_TOKEN\"}" .
17 | docker tag ${IMAGE_NAME}:${DATE} ${IMAGE_NAME}:latest
18 |
--------------------------------------------------------------------------------
/bin/docker_push.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Tags the existing Docker image and pushes the image to the Hub.
4 |
5 | # Example usage, to tag Version 1.2.3:
6 | # ./bin/docker_push.sh latest 1 1.2 1.2.3
7 |
8 | # Example usage, to tag Version 1.2.0-dev.4:
9 | # ./bin/docker_push.sh dev 2-dev 1.2-dev 1.2.0-dev 1.2.0-dev.4
10 |
11 | DATE=$(date +"%Y%m%d_%H%M%S")
12 | SCRIPT_BASEDIR=$(dirname "$0")
13 | versions=$*
14 |
15 |
16 | set -e
17 | which docker &> /dev/null || { echo 'ERROR: docker not found in PATH'; exit 1; }
18 |
19 | cd "${SCRIPT_BASEDIR}/.."
20 | source ./.env
21 |
22 | if [[ -z "$versions" ]]; then
23 | echo 'ERROR: no version given'
24 | exit 1
25 | fi
26 |
27 | for version in $versions ; do
28 | echo "Tag version: $version"
29 |
30 | # Tag
31 | docker tag ${IMAGE_NAME}:latest ${IMAGE_NAME}:${version}
32 |
33 | # Push Tags
34 | docker push ${IMAGE_NAME}:${version}
35 | done
36 |
--------------------------------------------------------------------------------
/bin/flickr-cli:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | add(new AlbumsCommand());
34 | $application->add(new AuthCommand());
35 | $application->add(new DeleteCommand());
36 | $application->add(new DownloadCommand());
37 | $application->add(new FilesCommand());
38 | $application->add(new UploadCommand());
39 | $application->add(new PiwigoCommand());
40 | $application->run();
41 |
--------------------------------------------------------------------------------
/bin/phpstan.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | SCRIPT_BASEDIR=$(dirname "$0")
4 |
5 |
6 | set -e
7 | cd "${SCRIPT_BASEDIR}/.."
8 |
9 | vendor/bin/phpstan analyse --no-progress --level 5 src bin/flickr-cli
10 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "thefox/flickr-cli",
3 | "description": "Upload and download Flickr photos, photo sets, directories via shell.",
4 | "license": "GPL-3.0",
5 | "type": "project",
6 | "keywords": [
7 | "Flickr",
8 | "Upload",
9 | "Download",
10 | "Photo",
11 | "CLI"
12 | ],
13 | "homepage": "https://github.com/TheFox/flickr-cli",
14 | "authors": [
15 | {
16 | "name": "Christian Mayer",
17 | "email": "christian@fox21.at",
18 | "homepage": "https://fox21.at"
19 | }
20 | ],
21 | "require": {
22 | "php": "^7.0",
23 | "rezzza/flickr": "^1.1",
24 | "symfony/yaml": "^2.3",
25 | "symfony/console": "^3.1",
26 | "symfony/filesystem": "^3.1",
27 | "symfony/finder": "^3.1",
28 | "monolog/monolog": "^1.21",
29 | "guzzlehttp/guzzle": "^3.8",
30 | "lusitanian/oauth": "^0.2",
31 | "rych/bytesize": "^1.0",
32 | "doctrine/dbal": "^2.5"
33 | },
34 | "require-dev": {
35 | "phpstan/phpstan": "^0.7",
36 | "squizlabs/php_codesniffer": "^3.0"
37 | },
38 | "autoload": {
39 | "psr-4": {
40 | "": "src"
41 | }
42 | },
43 | "bin": [
44 | "bin/flickr-cli"
45 | ]
46 | }
47 |
--------------------------------------------------------------------------------
/composer.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_readme": [
3 | "This file locks the dependencies of your project to a known state",
4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
5 | "This file is @generated automatically"
6 | ],
7 | "content-hash": "99ac10d80de82846470141b3fe88513e",
8 | "packages": [
9 | {
10 | "name": "doctrine/annotations",
11 | "version": "v1.4.0",
12 | "source": {
13 | "type": "git",
14 | "url": "https://github.com/doctrine/annotations.git",
15 | "reference": "54cacc9b81758b14e3ce750f205a393d52339e97"
16 | },
17 | "dist": {
18 | "type": "zip",
19 | "url": "https://api.github.com/repos/doctrine/annotations/zipball/54cacc9b81758b14e3ce750f205a393d52339e97",
20 | "reference": "54cacc9b81758b14e3ce750f205a393d52339e97",
21 | "shasum": ""
22 | },
23 | "require": {
24 | "doctrine/lexer": "1.*",
25 | "php": "^5.6 || ^7.0"
26 | },
27 | "require-dev": {
28 | "doctrine/cache": "1.*",
29 | "phpunit/phpunit": "^5.7"
30 | },
31 | "type": "library",
32 | "extra": {
33 | "branch-alias": {
34 | "dev-master": "1.4.x-dev"
35 | }
36 | },
37 | "autoload": {
38 | "psr-4": {
39 | "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations"
40 | }
41 | },
42 | "notification-url": "https://packagist.org/downloads/",
43 | "license": [
44 | "MIT"
45 | ],
46 | "authors": [
47 | {
48 | "name": "Roman Borschel",
49 | "email": "roman@code-factory.org"
50 | },
51 | {
52 | "name": "Benjamin Eberlei",
53 | "email": "kontakt@beberlei.de"
54 | },
55 | {
56 | "name": "Guilherme Blanco",
57 | "email": "guilhermeblanco@gmail.com"
58 | },
59 | {
60 | "name": "Jonathan Wage",
61 | "email": "jonwage@gmail.com"
62 | },
63 | {
64 | "name": "Johannes Schmitt",
65 | "email": "schmittjoh@gmail.com"
66 | }
67 | ],
68 | "description": "Docblock Annotations Parser",
69 | "homepage": "http://www.doctrine-project.org",
70 | "keywords": [
71 | "annotations",
72 | "docblock",
73 | "parser"
74 | ],
75 | "time": "2017-02-24T16:22:25+00:00"
76 | },
77 | {
78 | "name": "doctrine/cache",
79 | "version": "v1.6.2",
80 | "source": {
81 | "type": "git",
82 | "url": "https://github.com/doctrine/cache.git",
83 | "reference": "eb152c5100571c7a45470ff2a35095ab3f3b900b"
84 | },
85 | "dist": {
86 | "type": "zip",
87 | "url": "https://api.github.com/repos/doctrine/cache/zipball/eb152c5100571c7a45470ff2a35095ab3f3b900b",
88 | "reference": "eb152c5100571c7a45470ff2a35095ab3f3b900b",
89 | "shasum": ""
90 | },
91 | "require": {
92 | "php": "~5.5|~7.0"
93 | },
94 | "conflict": {
95 | "doctrine/common": ">2.2,<2.4"
96 | },
97 | "require-dev": {
98 | "phpunit/phpunit": "~4.8|~5.0",
99 | "predis/predis": "~1.0",
100 | "satooshi/php-coveralls": "~0.6"
101 | },
102 | "type": "library",
103 | "extra": {
104 | "branch-alias": {
105 | "dev-master": "1.6.x-dev"
106 | }
107 | },
108 | "autoload": {
109 | "psr-4": {
110 | "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache"
111 | }
112 | },
113 | "notification-url": "https://packagist.org/downloads/",
114 | "license": [
115 | "MIT"
116 | ],
117 | "authors": [
118 | {
119 | "name": "Roman Borschel",
120 | "email": "roman@code-factory.org"
121 | },
122 | {
123 | "name": "Benjamin Eberlei",
124 | "email": "kontakt@beberlei.de"
125 | },
126 | {
127 | "name": "Guilherme Blanco",
128 | "email": "guilhermeblanco@gmail.com"
129 | },
130 | {
131 | "name": "Jonathan Wage",
132 | "email": "jonwage@gmail.com"
133 | },
134 | {
135 | "name": "Johannes Schmitt",
136 | "email": "schmittjoh@gmail.com"
137 | }
138 | ],
139 | "description": "Caching library offering an object-oriented API for many cache backends",
140 | "homepage": "http://www.doctrine-project.org",
141 | "keywords": [
142 | "cache",
143 | "caching"
144 | ],
145 | "time": "2017-07-22T12:49:21+00:00"
146 | },
147 | {
148 | "name": "doctrine/collections",
149 | "version": "v1.4.0",
150 | "source": {
151 | "type": "git",
152 | "url": "https://github.com/doctrine/collections.git",
153 | "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba"
154 | },
155 | "dist": {
156 | "type": "zip",
157 | "url": "https://api.github.com/repos/doctrine/collections/zipball/1a4fb7e902202c33cce8c55989b945612943c2ba",
158 | "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba",
159 | "shasum": ""
160 | },
161 | "require": {
162 | "php": "^5.6 || ^7.0"
163 | },
164 | "require-dev": {
165 | "doctrine/coding-standard": "~0.1@dev",
166 | "phpunit/phpunit": "^5.7"
167 | },
168 | "type": "library",
169 | "extra": {
170 | "branch-alias": {
171 | "dev-master": "1.3.x-dev"
172 | }
173 | },
174 | "autoload": {
175 | "psr-0": {
176 | "Doctrine\\Common\\Collections\\": "lib/"
177 | }
178 | },
179 | "notification-url": "https://packagist.org/downloads/",
180 | "license": [
181 | "MIT"
182 | ],
183 | "authors": [
184 | {
185 | "name": "Roman Borschel",
186 | "email": "roman@code-factory.org"
187 | },
188 | {
189 | "name": "Benjamin Eberlei",
190 | "email": "kontakt@beberlei.de"
191 | },
192 | {
193 | "name": "Guilherme Blanco",
194 | "email": "guilhermeblanco@gmail.com"
195 | },
196 | {
197 | "name": "Jonathan Wage",
198 | "email": "jonwage@gmail.com"
199 | },
200 | {
201 | "name": "Johannes Schmitt",
202 | "email": "schmittjoh@gmail.com"
203 | }
204 | ],
205 | "description": "Collections Abstraction library",
206 | "homepage": "http://www.doctrine-project.org",
207 | "keywords": [
208 | "array",
209 | "collections",
210 | "iterator"
211 | ],
212 | "time": "2017-01-03T10:49:41+00:00"
213 | },
214 | {
215 | "name": "doctrine/common",
216 | "version": "v2.7.3",
217 | "source": {
218 | "type": "git",
219 | "url": "https://github.com/doctrine/common.git",
220 | "reference": "4acb8f89626baafede6ee5475bc5844096eba8a9"
221 | },
222 | "dist": {
223 | "type": "zip",
224 | "url": "https://api.github.com/repos/doctrine/common/zipball/4acb8f89626baafede6ee5475bc5844096eba8a9",
225 | "reference": "4acb8f89626baafede6ee5475bc5844096eba8a9",
226 | "shasum": ""
227 | },
228 | "require": {
229 | "doctrine/annotations": "1.*",
230 | "doctrine/cache": "1.*",
231 | "doctrine/collections": "1.*",
232 | "doctrine/inflector": "1.*",
233 | "doctrine/lexer": "1.*",
234 | "php": "~5.6|~7.0"
235 | },
236 | "require-dev": {
237 | "phpunit/phpunit": "^5.4.6"
238 | },
239 | "type": "library",
240 | "extra": {
241 | "branch-alias": {
242 | "dev-master": "2.7.x-dev"
243 | }
244 | },
245 | "autoload": {
246 | "psr-4": {
247 | "Doctrine\\Common\\": "lib/Doctrine/Common"
248 | }
249 | },
250 | "notification-url": "https://packagist.org/downloads/",
251 | "license": [
252 | "MIT"
253 | ],
254 | "authors": [
255 | {
256 | "name": "Roman Borschel",
257 | "email": "roman@code-factory.org"
258 | },
259 | {
260 | "name": "Benjamin Eberlei",
261 | "email": "kontakt@beberlei.de"
262 | },
263 | {
264 | "name": "Guilherme Blanco",
265 | "email": "guilhermeblanco@gmail.com"
266 | },
267 | {
268 | "name": "Jonathan Wage",
269 | "email": "jonwage@gmail.com"
270 | },
271 | {
272 | "name": "Johannes Schmitt",
273 | "email": "schmittjoh@gmail.com"
274 | }
275 | ],
276 | "description": "Common Library for Doctrine projects",
277 | "homepage": "http://www.doctrine-project.org",
278 | "keywords": [
279 | "annotations",
280 | "collections",
281 | "eventmanager",
282 | "persistence",
283 | "spl"
284 | ],
285 | "time": "2017-07-22T08:35:12+00:00"
286 | },
287 | {
288 | "name": "doctrine/dbal",
289 | "version": "v2.5.13",
290 | "source": {
291 | "type": "git",
292 | "url": "https://github.com/doctrine/dbal.git",
293 | "reference": "729340d8d1eec8f01bff708e12e449a3415af873"
294 | },
295 | "dist": {
296 | "type": "zip",
297 | "url": "https://api.github.com/repos/doctrine/dbal/zipball/729340d8d1eec8f01bff708e12e449a3415af873",
298 | "reference": "729340d8d1eec8f01bff708e12e449a3415af873",
299 | "shasum": ""
300 | },
301 | "require": {
302 | "doctrine/common": ">=2.4,<2.8-dev",
303 | "php": ">=5.3.2"
304 | },
305 | "require-dev": {
306 | "phpunit/phpunit": "4.*",
307 | "symfony/console": "2.*||^3.0"
308 | },
309 | "suggest": {
310 | "symfony/console": "For helpful console commands such as SQL execution and import of files."
311 | },
312 | "bin": [
313 | "bin/doctrine-dbal"
314 | ],
315 | "type": "library",
316 | "extra": {
317 | "branch-alias": {
318 | "dev-master": "2.5.x-dev"
319 | }
320 | },
321 | "autoload": {
322 | "psr-0": {
323 | "Doctrine\\DBAL\\": "lib/"
324 | }
325 | },
326 | "notification-url": "https://packagist.org/downloads/",
327 | "license": [
328 | "MIT"
329 | ],
330 | "authors": [
331 | {
332 | "name": "Roman Borschel",
333 | "email": "roman@code-factory.org"
334 | },
335 | {
336 | "name": "Benjamin Eberlei",
337 | "email": "kontakt@beberlei.de"
338 | },
339 | {
340 | "name": "Guilherme Blanco",
341 | "email": "guilhermeblanco@gmail.com"
342 | },
343 | {
344 | "name": "Jonathan Wage",
345 | "email": "jonwage@gmail.com"
346 | }
347 | ],
348 | "description": "Database Abstraction Layer",
349 | "homepage": "http://www.doctrine-project.org",
350 | "keywords": [
351 | "database",
352 | "dbal",
353 | "persistence",
354 | "queryobject"
355 | ],
356 | "time": "2017-07-22T20:44:48+00:00"
357 | },
358 | {
359 | "name": "doctrine/inflector",
360 | "version": "v1.2.0",
361 | "source": {
362 | "type": "git",
363 | "url": "https://github.com/doctrine/inflector.git",
364 | "reference": "e11d84c6e018beedd929cff5220969a3c6d1d462"
365 | },
366 | "dist": {
367 | "type": "zip",
368 | "url": "https://api.github.com/repos/doctrine/inflector/zipball/e11d84c6e018beedd929cff5220969a3c6d1d462",
369 | "reference": "e11d84c6e018beedd929cff5220969a3c6d1d462",
370 | "shasum": ""
371 | },
372 | "require": {
373 | "php": "^7.0"
374 | },
375 | "require-dev": {
376 | "phpunit/phpunit": "^6.2"
377 | },
378 | "type": "library",
379 | "extra": {
380 | "branch-alias": {
381 | "dev-master": "1.2.x-dev"
382 | }
383 | },
384 | "autoload": {
385 | "psr-4": {
386 | "Doctrine\\Common\\Inflector\\": "lib/Doctrine/Common/Inflector"
387 | }
388 | },
389 | "notification-url": "https://packagist.org/downloads/",
390 | "license": [
391 | "MIT"
392 | ],
393 | "authors": [
394 | {
395 | "name": "Roman Borschel",
396 | "email": "roman@code-factory.org"
397 | },
398 | {
399 | "name": "Benjamin Eberlei",
400 | "email": "kontakt@beberlei.de"
401 | },
402 | {
403 | "name": "Guilherme Blanco",
404 | "email": "guilhermeblanco@gmail.com"
405 | },
406 | {
407 | "name": "Jonathan Wage",
408 | "email": "jonwage@gmail.com"
409 | },
410 | {
411 | "name": "Johannes Schmitt",
412 | "email": "schmittjoh@gmail.com"
413 | }
414 | ],
415 | "description": "Common String Manipulations with regard to casing and singular/plural rules.",
416 | "homepage": "http://www.doctrine-project.org",
417 | "keywords": [
418 | "inflection",
419 | "pluralize",
420 | "singularize",
421 | "string"
422 | ],
423 | "time": "2017-07-22T12:18:28+00:00"
424 | },
425 | {
426 | "name": "doctrine/lexer",
427 | "version": "v1.0.1",
428 | "source": {
429 | "type": "git",
430 | "url": "https://github.com/doctrine/lexer.git",
431 | "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c"
432 | },
433 | "dist": {
434 | "type": "zip",
435 | "url": "https://api.github.com/repos/doctrine/lexer/zipball/83893c552fd2045dd78aef794c31e694c37c0b8c",
436 | "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c",
437 | "shasum": ""
438 | },
439 | "require": {
440 | "php": ">=5.3.2"
441 | },
442 | "type": "library",
443 | "extra": {
444 | "branch-alias": {
445 | "dev-master": "1.0.x-dev"
446 | }
447 | },
448 | "autoload": {
449 | "psr-0": {
450 | "Doctrine\\Common\\Lexer\\": "lib/"
451 | }
452 | },
453 | "notification-url": "https://packagist.org/downloads/",
454 | "license": [
455 | "MIT"
456 | ],
457 | "authors": [
458 | {
459 | "name": "Roman Borschel",
460 | "email": "roman@code-factory.org"
461 | },
462 | {
463 | "name": "Guilherme Blanco",
464 | "email": "guilhermeblanco@gmail.com"
465 | },
466 | {
467 | "name": "Johannes Schmitt",
468 | "email": "schmittjoh@gmail.com"
469 | }
470 | ],
471 | "description": "Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.",
472 | "homepage": "http://www.doctrine-project.org",
473 | "keywords": [
474 | "lexer",
475 | "parser"
476 | ],
477 | "time": "2014-09-09T13:34:57+00:00"
478 | },
479 | {
480 | "name": "guzzlehttp/guzzle",
481 | "version": "v3.8.1",
482 | "source": {
483 | "type": "git",
484 | "url": "https://github.com/guzzle/guzzle.git",
485 | "reference": "4de0618a01b34aa1c8c33a3f13f396dcd3882eba"
486 | },
487 | "dist": {
488 | "type": "zip",
489 | "url": "https://api.github.com/repos/guzzle/guzzle/zipball/4de0618a01b34aa1c8c33a3f13f396dcd3882eba",
490 | "reference": "4de0618a01b34aa1c8c33a3f13f396dcd3882eba",
491 | "shasum": ""
492 | },
493 | "require": {
494 | "ext-curl": "*",
495 | "php": ">=5.3.3",
496 | "symfony/event-dispatcher": ">=2.1"
497 | },
498 | "replace": {
499 | "guzzle/batch": "self.version",
500 | "guzzle/cache": "self.version",
501 | "guzzle/common": "self.version",
502 | "guzzle/http": "self.version",
503 | "guzzle/inflection": "self.version",
504 | "guzzle/iterator": "self.version",
505 | "guzzle/log": "self.version",
506 | "guzzle/parser": "self.version",
507 | "guzzle/plugin": "self.version",
508 | "guzzle/plugin-async": "self.version",
509 | "guzzle/plugin-backoff": "self.version",
510 | "guzzle/plugin-cache": "self.version",
511 | "guzzle/plugin-cookie": "self.version",
512 | "guzzle/plugin-curlauth": "self.version",
513 | "guzzle/plugin-error-response": "self.version",
514 | "guzzle/plugin-history": "self.version",
515 | "guzzle/plugin-log": "self.version",
516 | "guzzle/plugin-md5": "self.version",
517 | "guzzle/plugin-mock": "self.version",
518 | "guzzle/plugin-oauth": "self.version",
519 | "guzzle/service": "self.version",
520 | "guzzle/stream": "self.version"
521 | },
522 | "require-dev": {
523 | "doctrine/cache": "*",
524 | "monolog/monolog": "1.*",
525 | "phpunit/phpunit": "3.7.*",
526 | "psr/log": "1.0.*",
527 | "symfony/class-loader": "*",
528 | "zendframework/zend-cache": "<2.3",
529 | "zendframework/zend-log": "<2.3"
530 | },
531 | "type": "library",
532 | "extra": {
533 | "branch-alias": {
534 | "dev-master": "3.8-dev"
535 | }
536 | },
537 | "autoload": {
538 | "psr-0": {
539 | "Guzzle": "src/",
540 | "Guzzle\\Tests": "tests/"
541 | }
542 | },
543 | "notification-url": "https://packagist.org/downloads/",
544 | "license": [
545 | "MIT"
546 | ],
547 | "authors": [
548 | {
549 | "name": "Michael Dowling",
550 | "email": "mtdowling@gmail.com",
551 | "homepage": "https://github.com/mtdowling"
552 | },
553 | {
554 | "name": "Guzzle Community",
555 | "homepage": "https://github.com/guzzle/guzzle/contributors"
556 | }
557 | ],
558 | "description": "Guzzle is a PHP HTTP client library and framework for building RESTful web service clients",
559 | "homepage": "http://guzzlephp.org/",
560 | "keywords": [
561 | "client",
562 | "curl",
563 | "framework",
564 | "http",
565 | "http client",
566 | "rest",
567 | "web service"
568 | ],
569 | "time": "2014-01-28T22:29:15+00:00"
570 | },
571 | {
572 | "name": "lusitanian/oauth",
573 | "version": "v0.2.5",
574 | "source": {
575 | "type": "git",
576 | "url": "https://github.com/Lusitanian/PHPoAuthLib.git",
577 | "reference": "27e375e13e1badcd6dca7fb47b154b3c48fdec0c"
578 | },
579 | "dist": {
580 | "type": "zip",
581 | "url": "https://api.github.com/repos/Lusitanian/PHPoAuthLib/zipball/27e375e13e1badcd6dca7fb47b154b3c48fdec0c",
582 | "reference": "27e375e13e1badcd6dca7fb47b154b3c48fdec0c",
583 | "shasum": ""
584 | },
585 | "require": {
586 | "php": ">=5.3.0"
587 | },
588 | "require-dev": {
589 | "phpunit/phpunit": "3.7.*",
590 | "predis/predis": "0.8.*@dev",
591 | "symfony/http-foundation": "~2.1"
592 | },
593 | "suggest": {
594 | "predis/predis": "Allows using the Redis storage backend.",
595 | "symfony/http-foundation": "Allows using the Symfony Session storage backend."
596 | },
597 | "type": "library",
598 | "extra": {
599 | "branch-alias": {
600 | "dev-master": "0.1-dev"
601 | }
602 | },
603 | "autoload": {
604 | "psr-0": {
605 | "OAuth": "src",
606 | "OAuth\\Unit": "tests"
607 | }
608 | },
609 | "notification-url": "https://packagist.org/downloads/",
610 | "license": [
611 | "MIT"
612 | ],
613 | "authors": [
614 | {
615 | "name": "David Desberg",
616 | "email": "david@daviddesberg.com"
617 | },
618 | {
619 | "name": "Pieter Hordijk",
620 | "email": "info@pieterhordijk.com",
621 | "homepage": "https://pieterhordijk.com",
622 | "role": "Developer"
623 | }
624 | ],
625 | "description": "PHP 5.3+ oAuth 1/2 Library",
626 | "keywords": [
627 | "Authentication",
628 | "authorization",
629 | "oauth",
630 | "security"
631 | ],
632 | "time": "2013-12-25T20:05:42+00:00"
633 | },
634 | {
635 | "name": "monolog/monolog",
636 | "version": "1.23.0",
637 | "source": {
638 | "type": "git",
639 | "url": "https://github.com/Seldaek/monolog.git",
640 | "reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4"
641 | },
642 | "dist": {
643 | "type": "zip",
644 | "url": "https://api.github.com/repos/Seldaek/monolog/zipball/fd8c787753b3a2ad11bc60c063cff1358a32a3b4",
645 | "reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4",
646 | "shasum": ""
647 | },
648 | "require": {
649 | "php": ">=5.3.0",
650 | "psr/log": "~1.0"
651 | },
652 | "provide": {
653 | "psr/log-implementation": "1.0.0"
654 | },
655 | "require-dev": {
656 | "aws/aws-sdk-php": "^2.4.9 || ^3.0",
657 | "doctrine/couchdb": "~1.0@dev",
658 | "graylog2/gelf-php": "~1.0",
659 | "jakub-onderka/php-parallel-lint": "0.9",
660 | "php-amqplib/php-amqplib": "~2.4",
661 | "php-console/php-console": "^3.1.3",
662 | "phpunit/phpunit": "~4.5",
663 | "phpunit/phpunit-mock-objects": "2.3.0",
664 | "ruflin/elastica": ">=0.90 <3.0",
665 | "sentry/sentry": "^0.13",
666 | "swiftmailer/swiftmailer": "^5.3|^6.0"
667 | },
668 | "suggest": {
669 | "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
670 | "doctrine/couchdb": "Allow sending log messages to a CouchDB server",
671 | "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
672 | "ext-mongo": "Allow sending log messages to a MongoDB server",
673 | "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
674 | "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver",
675 | "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
676 | "php-console/php-console": "Allow sending log messages to Google Chrome",
677 | "rollbar/rollbar": "Allow sending log messages to Rollbar",
678 | "ruflin/elastica": "Allow sending log messages to an Elastic Search server",
679 | "sentry/sentry": "Allow sending log messages to a Sentry server"
680 | },
681 | "type": "library",
682 | "extra": {
683 | "branch-alias": {
684 | "dev-master": "2.0.x-dev"
685 | }
686 | },
687 | "autoload": {
688 | "psr-4": {
689 | "Monolog\\": "src/Monolog"
690 | }
691 | },
692 | "notification-url": "https://packagist.org/downloads/",
693 | "license": [
694 | "MIT"
695 | ],
696 | "authors": [
697 | {
698 | "name": "Jordi Boggiano",
699 | "email": "j.boggiano@seld.be",
700 | "homepage": "http://seld.be"
701 | }
702 | ],
703 | "description": "Sends your logs to files, sockets, inboxes, databases and various web services",
704 | "homepage": "http://github.com/Seldaek/monolog",
705 | "keywords": [
706 | "log",
707 | "logging",
708 | "psr-3"
709 | ],
710 | "time": "2017-06-19T01:22:40+00:00"
711 | },
712 | {
713 | "name": "psr/log",
714 | "version": "1.0.2",
715 | "source": {
716 | "type": "git",
717 | "url": "https://github.com/php-fig/log.git",
718 | "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d"
719 | },
720 | "dist": {
721 | "type": "zip",
722 | "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d",
723 | "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d",
724 | "shasum": ""
725 | },
726 | "require": {
727 | "php": ">=5.3.0"
728 | },
729 | "type": "library",
730 | "extra": {
731 | "branch-alias": {
732 | "dev-master": "1.0.x-dev"
733 | }
734 | },
735 | "autoload": {
736 | "psr-4": {
737 | "Psr\\Log\\": "Psr/Log/"
738 | }
739 | },
740 | "notification-url": "https://packagist.org/downloads/",
741 | "license": [
742 | "MIT"
743 | ],
744 | "authors": [
745 | {
746 | "name": "PHP-FIG",
747 | "homepage": "http://www.php-fig.org/"
748 | }
749 | ],
750 | "description": "Common interface for logging libraries",
751 | "homepage": "https://github.com/php-fig/log",
752 | "keywords": [
753 | "log",
754 | "psr",
755 | "psr-3"
756 | ],
757 | "time": "2016-10-10T12:19:37+00:00"
758 | },
759 | {
760 | "name": "rezzza/flickr",
761 | "version": "v1.1.0",
762 | "source": {
763 | "type": "git",
764 | "url": "https://github.com/rezzza/flickr.git",
765 | "reference": "97bf6ce57614ac793a6aaecf887a0da1dcc9c281"
766 | },
767 | "dist": {
768 | "type": "zip",
769 | "url": "https://api.github.com/repos/rezzza/flickr/zipball/97bf6ce57614ac793a6aaecf887a0da1dcc9c281",
770 | "reference": "97bf6ce57614ac793a6aaecf887a0da1dcc9c281",
771 | "shasum": ""
772 | },
773 | "require": {
774 | "php": ">=5.3.2"
775 | },
776 | "require-dev": {
777 | "guzzle/http": "3.*",
778 | "phpunit/phpunit": "3.7.32"
779 | },
780 | "suggest": {
781 | "guzzle/guzzle": "HTTP wrapper",
782 | "guzzle/http": "Guzzle Adapter for http requests"
783 | },
784 | "type": "standalone",
785 | "extra": {
786 | "branch-alias": {
787 | "dev-master": "1.0.x-dev"
788 | }
789 | },
790 | "autoload": {
791 | "psr-0": {
792 | "Rezzza\\Flickr": "src/"
793 | }
794 | },
795 | "notification-url": "https://packagist.org/downloads/",
796 | "license": [
797 | "MIT"
798 | ],
799 | "authors": [
800 | {
801 | "name": "Community contributors",
802 | "homepage": "https://github.com/rezzza/flickr/contributors"
803 | }
804 | ],
805 | "description": "Flickr API Wrapper",
806 | "homepage": "https://github.com/rezzza/flickr",
807 | "keywords": [
808 | "flickr"
809 | ],
810 | "time": "2014-02-26T09:18:38+00:00"
811 | },
812 | {
813 | "name": "rych/bytesize",
814 | "version": "v1.0.0",
815 | "source": {
816 | "type": "git",
817 | "url": "https://github.com/rchouinard/bytesize.git",
818 | "reference": "297e16ea047461b91e8d7eb90aa46aaa52917824"
819 | },
820 | "dist": {
821 | "type": "zip",
822 | "url": "https://api.github.com/repos/rchouinard/bytesize/zipball/297e16ea047461b91e8d7eb90aa46aaa52917824",
823 | "reference": "297e16ea047461b91e8d7eb90aa46aaa52917824",
824 | "shasum": ""
825 | },
826 | "require": {
827 | "ext-bcmath": "*",
828 | "php": ">=5.3.4"
829 | },
830 | "require-dev": {
831 | "phpunit/phpunit": "3.7.*"
832 | },
833 | "type": "library",
834 | "autoload": {
835 | "psr-4": {
836 | "Rych\\ByteSize\\": "src/"
837 | }
838 | },
839 | "notification-url": "https://packagist.org/downloads/",
840 | "license": [
841 | "MIT"
842 | ],
843 | "authors": [
844 | {
845 | "name": "Ryan Chouinard",
846 | "email": "rchouinard@gmail.com",
847 | "homepage": "http://ryanchouinard.com"
848 | }
849 | ],
850 | "description": "Utility component for nicely formatted file sizes.",
851 | "homepage": "https://github.com/rchouinard/bytesize",
852 | "keywords": [
853 | "filesize"
854 | ],
855 | "time": "2014-04-04T18:06:18+00:00"
856 | },
857 | {
858 | "name": "symfony/console",
859 | "version": "v3.3.13",
860 | "source": {
861 | "type": "git",
862 | "url": "https://github.com/symfony/console.git",
863 | "reference": "63cd7960a0a522c3537f6326706d7f3b8de65805"
864 | },
865 | "dist": {
866 | "type": "zip",
867 | "url": "https://api.github.com/repos/symfony/console/zipball/63cd7960a0a522c3537f6326706d7f3b8de65805",
868 | "reference": "63cd7960a0a522c3537f6326706d7f3b8de65805",
869 | "shasum": ""
870 | },
871 | "require": {
872 | "php": "^5.5.9|>=7.0.8",
873 | "symfony/debug": "~2.8|~3.0",
874 | "symfony/polyfill-mbstring": "~1.0"
875 | },
876 | "conflict": {
877 | "symfony/dependency-injection": "<3.3"
878 | },
879 | "require-dev": {
880 | "psr/log": "~1.0",
881 | "symfony/config": "~3.3",
882 | "symfony/dependency-injection": "~3.3",
883 | "symfony/event-dispatcher": "~2.8|~3.0",
884 | "symfony/filesystem": "~2.8|~3.0",
885 | "symfony/process": "~2.8|~3.0"
886 | },
887 | "suggest": {
888 | "psr/log": "For using the console logger",
889 | "symfony/event-dispatcher": "",
890 | "symfony/filesystem": "",
891 | "symfony/process": ""
892 | },
893 | "type": "library",
894 | "extra": {
895 | "branch-alias": {
896 | "dev-master": "3.3-dev"
897 | }
898 | },
899 | "autoload": {
900 | "psr-4": {
901 | "Symfony\\Component\\Console\\": ""
902 | },
903 | "exclude-from-classmap": [
904 | "/Tests/"
905 | ]
906 | },
907 | "notification-url": "https://packagist.org/downloads/",
908 | "license": [
909 | "MIT"
910 | ],
911 | "authors": [
912 | {
913 | "name": "Fabien Potencier",
914 | "email": "fabien@symfony.com"
915 | },
916 | {
917 | "name": "Symfony Community",
918 | "homepage": "https://symfony.com/contributors"
919 | }
920 | ],
921 | "description": "Symfony Console Component",
922 | "homepage": "https://symfony.com",
923 | "time": "2017-11-16T15:24:32+00:00"
924 | },
925 | {
926 | "name": "symfony/debug",
927 | "version": "v3.3.13",
928 | "source": {
929 | "type": "git",
930 | "url": "https://github.com/symfony/debug.git",
931 | "reference": "74557880e2846b5c84029faa96b834da37e29810"
932 | },
933 | "dist": {
934 | "type": "zip",
935 | "url": "https://api.github.com/repos/symfony/debug/zipball/74557880e2846b5c84029faa96b834da37e29810",
936 | "reference": "74557880e2846b5c84029faa96b834da37e29810",
937 | "shasum": ""
938 | },
939 | "require": {
940 | "php": "^5.5.9|>=7.0.8",
941 | "psr/log": "~1.0"
942 | },
943 | "conflict": {
944 | "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2"
945 | },
946 | "require-dev": {
947 | "symfony/http-kernel": "~2.8|~3.0"
948 | },
949 | "type": "library",
950 | "extra": {
951 | "branch-alias": {
952 | "dev-master": "3.3-dev"
953 | }
954 | },
955 | "autoload": {
956 | "psr-4": {
957 | "Symfony\\Component\\Debug\\": ""
958 | },
959 | "exclude-from-classmap": [
960 | "/Tests/"
961 | ]
962 | },
963 | "notification-url": "https://packagist.org/downloads/",
964 | "license": [
965 | "MIT"
966 | ],
967 | "authors": [
968 | {
969 | "name": "Fabien Potencier",
970 | "email": "fabien@symfony.com"
971 | },
972 | {
973 | "name": "Symfony Community",
974 | "homepage": "https://symfony.com/contributors"
975 | }
976 | ],
977 | "description": "Symfony Debug Component",
978 | "homepage": "https://symfony.com",
979 | "time": "2017-11-10T16:38:39+00:00"
980 | },
981 | {
982 | "name": "symfony/event-dispatcher",
983 | "version": "v3.3.13",
984 | "source": {
985 | "type": "git",
986 | "url": "https://github.com/symfony/event-dispatcher.git",
987 | "reference": "271d8c27c3ec5ecee6e2ac06016232e249d638d9"
988 | },
989 | "dist": {
990 | "type": "zip",
991 | "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/271d8c27c3ec5ecee6e2ac06016232e249d638d9",
992 | "reference": "271d8c27c3ec5ecee6e2ac06016232e249d638d9",
993 | "shasum": ""
994 | },
995 | "require": {
996 | "php": "^5.5.9|>=7.0.8"
997 | },
998 | "conflict": {
999 | "symfony/dependency-injection": "<3.3"
1000 | },
1001 | "require-dev": {
1002 | "psr/log": "~1.0",
1003 | "symfony/config": "~2.8|~3.0",
1004 | "symfony/dependency-injection": "~3.3",
1005 | "symfony/expression-language": "~2.8|~3.0",
1006 | "symfony/stopwatch": "~2.8|~3.0"
1007 | },
1008 | "suggest": {
1009 | "symfony/dependency-injection": "",
1010 | "symfony/http-kernel": ""
1011 | },
1012 | "type": "library",
1013 | "extra": {
1014 | "branch-alias": {
1015 | "dev-master": "3.3-dev"
1016 | }
1017 | },
1018 | "autoload": {
1019 | "psr-4": {
1020 | "Symfony\\Component\\EventDispatcher\\": ""
1021 | },
1022 | "exclude-from-classmap": [
1023 | "/Tests/"
1024 | ]
1025 | },
1026 | "notification-url": "https://packagist.org/downloads/",
1027 | "license": [
1028 | "MIT"
1029 | ],
1030 | "authors": [
1031 | {
1032 | "name": "Fabien Potencier",
1033 | "email": "fabien@symfony.com"
1034 | },
1035 | {
1036 | "name": "Symfony Community",
1037 | "homepage": "https://symfony.com/contributors"
1038 | }
1039 | ],
1040 | "description": "Symfony EventDispatcher Component",
1041 | "homepage": "https://symfony.com",
1042 | "time": "2017-11-05T15:47:03+00:00"
1043 | },
1044 | {
1045 | "name": "symfony/filesystem",
1046 | "version": "v3.3.13",
1047 | "source": {
1048 | "type": "git",
1049 | "url": "https://github.com/symfony/filesystem.git",
1050 | "reference": "77db266766b54db3ee982fe51868328b887ce15c"
1051 | },
1052 | "dist": {
1053 | "type": "zip",
1054 | "url": "https://api.github.com/repos/symfony/filesystem/zipball/77db266766b54db3ee982fe51868328b887ce15c",
1055 | "reference": "77db266766b54db3ee982fe51868328b887ce15c",
1056 | "shasum": ""
1057 | },
1058 | "require": {
1059 | "php": "^5.5.9|>=7.0.8"
1060 | },
1061 | "type": "library",
1062 | "extra": {
1063 | "branch-alias": {
1064 | "dev-master": "3.3-dev"
1065 | }
1066 | },
1067 | "autoload": {
1068 | "psr-4": {
1069 | "Symfony\\Component\\Filesystem\\": ""
1070 | },
1071 | "exclude-from-classmap": [
1072 | "/Tests/"
1073 | ]
1074 | },
1075 | "notification-url": "https://packagist.org/downloads/",
1076 | "license": [
1077 | "MIT"
1078 | ],
1079 | "authors": [
1080 | {
1081 | "name": "Fabien Potencier",
1082 | "email": "fabien@symfony.com"
1083 | },
1084 | {
1085 | "name": "Symfony Community",
1086 | "homepage": "https://symfony.com/contributors"
1087 | }
1088 | ],
1089 | "description": "Symfony Filesystem Component",
1090 | "homepage": "https://symfony.com",
1091 | "time": "2017-11-07T14:12:55+00:00"
1092 | },
1093 | {
1094 | "name": "symfony/finder",
1095 | "version": "v3.3.13",
1096 | "source": {
1097 | "type": "git",
1098 | "url": "https://github.com/symfony/finder.git",
1099 | "reference": "138af5ec075d4b1d1bd19de08c38a34bb2d7d880"
1100 | },
1101 | "dist": {
1102 | "type": "zip",
1103 | "url": "https://api.github.com/repos/symfony/finder/zipball/138af5ec075d4b1d1bd19de08c38a34bb2d7d880",
1104 | "reference": "138af5ec075d4b1d1bd19de08c38a34bb2d7d880",
1105 | "shasum": ""
1106 | },
1107 | "require": {
1108 | "php": "^5.5.9|>=7.0.8"
1109 | },
1110 | "type": "library",
1111 | "extra": {
1112 | "branch-alias": {
1113 | "dev-master": "3.3-dev"
1114 | }
1115 | },
1116 | "autoload": {
1117 | "psr-4": {
1118 | "Symfony\\Component\\Finder\\": ""
1119 | },
1120 | "exclude-from-classmap": [
1121 | "/Tests/"
1122 | ]
1123 | },
1124 | "notification-url": "https://packagist.org/downloads/",
1125 | "license": [
1126 | "MIT"
1127 | ],
1128 | "authors": [
1129 | {
1130 | "name": "Fabien Potencier",
1131 | "email": "fabien@symfony.com"
1132 | },
1133 | {
1134 | "name": "Symfony Community",
1135 | "homepage": "https://symfony.com/contributors"
1136 | }
1137 | ],
1138 | "description": "Symfony Finder Component",
1139 | "homepage": "https://symfony.com",
1140 | "time": "2017-11-05T15:47:03+00:00"
1141 | },
1142 | {
1143 | "name": "symfony/polyfill-mbstring",
1144 | "version": "v1.6.0",
1145 | "source": {
1146 | "type": "git",
1147 | "url": "https://github.com/symfony/polyfill-mbstring.git",
1148 | "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296"
1149 | },
1150 | "dist": {
1151 | "type": "zip",
1152 | "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296",
1153 | "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296",
1154 | "shasum": ""
1155 | },
1156 | "require": {
1157 | "php": ">=5.3.3"
1158 | },
1159 | "suggest": {
1160 | "ext-mbstring": "For best performance"
1161 | },
1162 | "type": "library",
1163 | "extra": {
1164 | "branch-alias": {
1165 | "dev-master": "1.6-dev"
1166 | }
1167 | },
1168 | "autoload": {
1169 | "psr-4": {
1170 | "Symfony\\Polyfill\\Mbstring\\": ""
1171 | },
1172 | "files": [
1173 | "bootstrap.php"
1174 | ]
1175 | },
1176 | "notification-url": "https://packagist.org/downloads/",
1177 | "license": [
1178 | "MIT"
1179 | ],
1180 | "authors": [
1181 | {
1182 | "name": "Nicolas Grekas",
1183 | "email": "p@tchwork.com"
1184 | },
1185 | {
1186 | "name": "Symfony Community",
1187 | "homepage": "https://symfony.com/contributors"
1188 | }
1189 | ],
1190 | "description": "Symfony polyfill for the Mbstring extension",
1191 | "homepage": "https://symfony.com",
1192 | "keywords": [
1193 | "compatibility",
1194 | "mbstring",
1195 | "polyfill",
1196 | "portable",
1197 | "shim"
1198 | ],
1199 | "time": "2017-10-11T12:05:26+00:00"
1200 | },
1201 | {
1202 | "name": "symfony/yaml",
1203 | "version": "v2.8.31",
1204 | "source": {
1205 | "type": "git",
1206 | "url": "https://github.com/symfony/yaml.git",
1207 | "reference": "d819bf267e901727141fe828ae888486fd21236e"
1208 | },
1209 | "dist": {
1210 | "type": "zip",
1211 | "url": "https://api.github.com/repos/symfony/yaml/zipball/d819bf267e901727141fe828ae888486fd21236e",
1212 | "reference": "d819bf267e901727141fe828ae888486fd21236e",
1213 | "shasum": ""
1214 | },
1215 | "require": {
1216 | "php": ">=5.3.9"
1217 | },
1218 | "type": "library",
1219 | "extra": {
1220 | "branch-alias": {
1221 | "dev-master": "2.8-dev"
1222 | }
1223 | },
1224 | "autoload": {
1225 | "psr-4": {
1226 | "Symfony\\Component\\Yaml\\": ""
1227 | },
1228 | "exclude-from-classmap": [
1229 | "/Tests/"
1230 | ]
1231 | },
1232 | "notification-url": "https://packagist.org/downloads/",
1233 | "license": [
1234 | "MIT"
1235 | ],
1236 | "authors": [
1237 | {
1238 | "name": "Fabien Potencier",
1239 | "email": "fabien@symfony.com"
1240 | },
1241 | {
1242 | "name": "Symfony Community",
1243 | "homepage": "https://symfony.com/contributors"
1244 | }
1245 | ],
1246 | "description": "Symfony Yaml Component",
1247 | "homepage": "https://symfony.com",
1248 | "time": "2017-11-05T15:25:56+00:00"
1249 | }
1250 | ],
1251 | "packages-dev": [
1252 | {
1253 | "name": "nette/bootstrap",
1254 | "version": "v2.4.5",
1255 | "source": {
1256 | "type": "git",
1257 | "url": "https://github.com/nette/bootstrap.git",
1258 | "reference": "804925787764d708a7782ea0d9382a310bb21968"
1259 | },
1260 | "dist": {
1261 | "type": "zip",
1262 | "url": "https://api.github.com/repos/nette/bootstrap/zipball/804925787764d708a7782ea0d9382a310bb21968",
1263 | "reference": "804925787764d708a7782ea0d9382a310bb21968",
1264 | "shasum": ""
1265 | },
1266 | "require": {
1267 | "nette/di": "~2.4.7",
1268 | "nette/utils": "~2.4",
1269 | "php": ">=5.6.0"
1270 | },
1271 | "conflict": {
1272 | "nette/nette": "<2.2"
1273 | },
1274 | "require-dev": {
1275 | "latte/latte": "~2.2",
1276 | "nette/application": "~2.3",
1277 | "nette/caching": "~2.3",
1278 | "nette/database": "~2.3",
1279 | "nette/forms": "~2.3",
1280 | "nette/http": "~2.4.0",
1281 | "nette/mail": "~2.3",
1282 | "nette/robot-loader": "^2.4.2 || ^3.0",
1283 | "nette/safe-stream": "~2.2",
1284 | "nette/security": "~2.3",
1285 | "nette/tester": "~2.0",
1286 | "tracy/tracy": "^2.4.1"
1287 | },
1288 | "suggest": {
1289 | "nette/robot-loader": "to use Configurator::createRobotLoader()",
1290 | "tracy/tracy": "to use Configurator::enableTracy()"
1291 | },
1292 | "type": "library",
1293 | "extra": {
1294 | "branch-alias": {
1295 | "dev-master": "2.4-dev"
1296 | }
1297 | },
1298 | "autoload": {
1299 | "classmap": [
1300 | "src/"
1301 | ]
1302 | },
1303 | "notification-url": "https://packagist.org/downloads/",
1304 | "license": [
1305 | "BSD-3-Clause",
1306 | "GPL-2.0",
1307 | "GPL-3.0"
1308 | ],
1309 | "authors": [
1310 | {
1311 | "name": "David Grudl",
1312 | "homepage": "https://davidgrudl.com"
1313 | },
1314 | {
1315 | "name": "Nette Community",
1316 | "homepage": "https://nette.org/contributors"
1317 | }
1318 | ],
1319 | "description": "🅱 Nette Bootstrap: the simple way to configure and bootstrap your Nette application.",
1320 | "homepage": "https://nette.org",
1321 | "keywords": [
1322 | "bootstrapping",
1323 | "configurator",
1324 | "nette"
1325 | ],
1326 | "time": "2017-08-20T17:36:59+00:00"
1327 | },
1328 | {
1329 | "name": "nette/caching",
1330 | "version": "v2.5.6",
1331 | "source": {
1332 | "type": "git",
1333 | "url": "https://github.com/nette/caching.git",
1334 | "reference": "1231735b5135ca02bd381b70482c052d2a90bdc9"
1335 | },
1336 | "dist": {
1337 | "type": "zip",
1338 | "url": "https://api.github.com/repos/nette/caching/zipball/1231735b5135ca02bd381b70482c052d2a90bdc9",
1339 | "reference": "1231735b5135ca02bd381b70482c052d2a90bdc9",
1340 | "shasum": ""
1341 | },
1342 | "require": {
1343 | "nette/finder": "^2.2 || ~3.0.0",
1344 | "nette/utils": "^2.4 || ~3.0.0",
1345 | "php": ">=5.6.0"
1346 | },
1347 | "conflict": {
1348 | "nette/nette": "<2.2"
1349 | },
1350 | "require-dev": {
1351 | "latte/latte": "^2.4",
1352 | "nette/di": "^2.4 || ~3.0.0",
1353 | "nette/tester": "^2.0",
1354 | "tracy/tracy": "^2.4"
1355 | },
1356 | "suggest": {
1357 | "ext-pdo_sqlite": "to use SQLiteStorage or SQLiteJournal"
1358 | },
1359 | "type": "library",
1360 | "extra": {
1361 | "branch-alias": {
1362 | "dev-master": "2.5-dev"
1363 | }
1364 | },
1365 | "autoload": {
1366 | "classmap": [
1367 | "src/"
1368 | ]
1369 | },
1370 | "notification-url": "https://packagist.org/downloads/",
1371 | "license": [
1372 | "BSD-3-Clause",
1373 | "GPL-2.0",
1374 | "GPL-3.0"
1375 | ],
1376 | "authors": [
1377 | {
1378 | "name": "David Grudl",
1379 | "homepage": "https://davidgrudl.com"
1380 | },
1381 | {
1382 | "name": "Nette Community",
1383 | "homepage": "https://nette.org/contributors"
1384 | }
1385 | ],
1386 | "description": "⏱ Nette Caching: library with easy-to-use API and many cache backends.",
1387 | "homepage": "https://nette.org",
1388 | "keywords": [
1389 | "cache",
1390 | "journal",
1391 | "memcached",
1392 | "nette",
1393 | "sqlite"
1394 | ],
1395 | "time": "2017-08-30T12:12:25+00:00"
1396 | },
1397 | {
1398 | "name": "nette/di",
1399 | "version": "v2.4.10",
1400 | "source": {
1401 | "type": "git",
1402 | "url": "https://github.com/nette/di.git",
1403 | "reference": "a4b3be935b755f23aebea1ce33d7e3c832cdff98"
1404 | },
1405 | "dist": {
1406 | "type": "zip",
1407 | "url": "https://api.github.com/repos/nette/di/zipball/a4b3be935b755f23aebea1ce33d7e3c832cdff98",
1408 | "reference": "a4b3be935b755f23aebea1ce33d7e3c832cdff98",
1409 | "shasum": ""
1410 | },
1411 | "require": {
1412 | "ext-tokenizer": "*",
1413 | "nette/neon": "^2.3.3 || ~3.0.0",
1414 | "nette/php-generator": "^2.6.1 || ~3.0.0",
1415 | "nette/utils": "^2.4.3 || ~3.0.0",
1416 | "php": ">=5.6.0"
1417 | },
1418 | "conflict": {
1419 | "nette/bootstrap": "<2.4",
1420 | "nette/nette": "<2.2"
1421 | },
1422 | "require-dev": {
1423 | "nette/tester": "^2.0",
1424 | "tracy/tracy": "^2.3"
1425 | },
1426 | "type": "library",
1427 | "extra": {
1428 | "branch-alias": {
1429 | "dev-master": "2.4-dev"
1430 | }
1431 | },
1432 | "autoload": {
1433 | "classmap": [
1434 | "src/"
1435 | ]
1436 | },
1437 | "notification-url": "https://packagist.org/downloads/",
1438 | "license": [
1439 | "BSD-3-Clause",
1440 | "GPL-2.0",
1441 | "GPL-3.0"
1442 | ],
1443 | "authors": [
1444 | {
1445 | "name": "David Grudl",
1446 | "homepage": "https://davidgrudl.com"
1447 | },
1448 | {
1449 | "name": "Nette Community",
1450 | "homepage": "https://nette.org/contributors"
1451 | }
1452 | ],
1453 | "description": "💎 Nette Dependency Injection Container: Flexible, compiled and full-featured DIC with perfectly usable autowiring and support for all new PHP 7.1 features.",
1454 | "homepage": "https://nette.org",
1455 | "keywords": [
1456 | "compiled",
1457 | "di",
1458 | "dic",
1459 | "factory",
1460 | "ioc",
1461 | "nette",
1462 | "static"
1463 | ],
1464 | "time": "2017-08-31T22:42:00+00:00"
1465 | },
1466 | {
1467 | "name": "nette/finder",
1468 | "version": "v2.4.1",
1469 | "source": {
1470 | "type": "git",
1471 | "url": "https://github.com/nette/finder.git",
1472 | "reference": "4d43a66d072c57d585bf08a3ef68d3587f7e9547"
1473 | },
1474 | "dist": {
1475 | "type": "zip",
1476 | "url": "https://api.github.com/repos/nette/finder/zipball/4d43a66d072c57d585bf08a3ef68d3587f7e9547",
1477 | "reference": "4d43a66d072c57d585bf08a3ef68d3587f7e9547",
1478 | "shasum": ""
1479 | },
1480 | "require": {
1481 | "nette/utils": "^2.4 || ~3.0.0",
1482 | "php": ">=5.6.0"
1483 | },
1484 | "conflict": {
1485 | "nette/nette": "<2.2"
1486 | },
1487 | "require-dev": {
1488 | "nette/tester": "^2.0",
1489 | "tracy/tracy": "^2.3"
1490 | },
1491 | "type": "library",
1492 | "extra": {
1493 | "branch-alias": {
1494 | "dev-master": "2.4-dev"
1495 | }
1496 | },
1497 | "autoload": {
1498 | "classmap": [
1499 | "src/"
1500 | ]
1501 | },
1502 | "notification-url": "https://packagist.org/downloads/",
1503 | "license": [
1504 | "BSD-3-Clause",
1505 | "GPL-2.0",
1506 | "GPL-3.0"
1507 | ],
1508 | "authors": [
1509 | {
1510 | "name": "David Grudl",
1511 | "homepage": "https://davidgrudl.com"
1512 | },
1513 | {
1514 | "name": "Nette Community",
1515 | "homepage": "https://nette.org/contributors"
1516 | }
1517 | ],
1518 | "description": "Nette Finder: Files Searching",
1519 | "homepage": "https://nette.org",
1520 | "time": "2017-07-10T23:47:08+00:00"
1521 | },
1522 | {
1523 | "name": "nette/neon",
1524 | "version": "v2.4.2",
1525 | "source": {
1526 | "type": "git",
1527 | "url": "https://github.com/nette/neon.git",
1528 | "reference": "9eacd50553b26b53a3977bfb2fea2166d4331622"
1529 | },
1530 | "dist": {
1531 | "type": "zip",
1532 | "url": "https://api.github.com/repos/nette/neon/zipball/9eacd50553b26b53a3977bfb2fea2166d4331622",
1533 | "reference": "9eacd50553b26b53a3977bfb2fea2166d4331622",
1534 | "shasum": ""
1535 | },
1536 | "require": {
1537 | "ext-iconv": "*",
1538 | "ext-json": "*",
1539 | "php": ">=5.6.0"
1540 | },
1541 | "require-dev": {
1542 | "nette/tester": "~2.0",
1543 | "tracy/tracy": "^2.3"
1544 | },
1545 | "type": "library",
1546 | "extra": {
1547 | "branch-alias": {
1548 | "dev-master": "2.4-dev"
1549 | }
1550 | },
1551 | "autoload": {
1552 | "classmap": [
1553 | "src/"
1554 | ]
1555 | },
1556 | "notification-url": "https://packagist.org/downloads/",
1557 | "license": [
1558 | "BSD-3-Clause",
1559 | "GPL-2.0",
1560 | "GPL-3.0"
1561 | ],
1562 | "authors": [
1563 | {
1564 | "name": "David Grudl",
1565 | "homepage": "https://davidgrudl.com"
1566 | },
1567 | {
1568 | "name": "Nette Community",
1569 | "homepage": "https://nette.org/contributors"
1570 | }
1571 | ],
1572 | "description": "Nette NEON: parser & generator for Nette Object Notation",
1573 | "homepage": "http://ne-on.org",
1574 | "time": "2017-07-11T18:29:08+00:00"
1575 | },
1576 | {
1577 | "name": "nette/php-generator",
1578 | "version": "v3.0.1",
1579 | "source": {
1580 | "type": "git",
1581 | "url": "https://github.com/nette/php-generator.git",
1582 | "reference": "eb2dbc9c3409e9db40568109ca4994d51373b60c"
1583 | },
1584 | "dist": {
1585 | "type": "zip",
1586 | "url": "https://api.github.com/repos/nette/php-generator/zipball/eb2dbc9c3409e9db40568109ca4994d51373b60c",
1587 | "reference": "eb2dbc9c3409e9db40568109ca4994d51373b60c",
1588 | "shasum": ""
1589 | },
1590 | "require": {
1591 | "nette/utils": "^2.4.2 || ~3.0.0",
1592 | "php": ">=7.0"
1593 | },
1594 | "conflict": {
1595 | "nette/nette": "<2.2"
1596 | },
1597 | "require-dev": {
1598 | "nette/tester": "^2.0",
1599 | "tracy/tracy": "^2.3"
1600 | },
1601 | "type": "library",
1602 | "extra": {
1603 | "branch-alias": {
1604 | "dev-master": "3.0-dev"
1605 | }
1606 | },
1607 | "autoload": {
1608 | "classmap": [
1609 | "src/"
1610 | ]
1611 | },
1612 | "notification-url": "https://packagist.org/downloads/",
1613 | "license": [
1614 | "BSD-3-Clause",
1615 | "GPL-2.0",
1616 | "GPL-3.0"
1617 | ],
1618 | "authors": [
1619 | {
1620 | "name": "David Grudl",
1621 | "homepage": "https://davidgrudl.com"
1622 | },
1623 | {
1624 | "name": "Nette Community",
1625 | "homepage": "https://nette.org/contributors"
1626 | }
1627 | ],
1628 | "description": "🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 7.1 features.",
1629 | "homepage": "https://nette.org",
1630 | "keywords": [
1631 | "code",
1632 | "nette",
1633 | "php",
1634 | "scaffolding"
1635 | ],
1636 | "time": "2017-07-11T19:07:13+00:00"
1637 | },
1638 | {
1639 | "name": "nette/robot-loader",
1640 | "version": "v3.0.2",
1641 | "source": {
1642 | "type": "git",
1643 | "url": "https://github.com/nette/robot-loader.git",
1644 | "reference": "b703b4f5955831b0bcaacbd2f6af76021b056826"
1645 | },
1646 | "dist": {
1647 | "type": "zip",
1648 | "url": "https://api.github.com/repos/nette/robot-loader/zipball/b703b4f5955831b0bcaacbd2f6af76021b056826",
1649 | "reference": "b703b4f5955831b0bcaacbd2f6af76021b056826",
1650 | "shasum": ""
1651 | },
1652 | "require": {
1653 | "ext-tokenizer": "*",
1654 | "nette/finder": "^2.3 || ^3.0",
1655 | "nette/utils": "^2.4 || ^3.0",
1656 | "php": ">=5.6.0"
1657 | },
1658 | "conflict": {
1659 | "nette/nette": "<2.2"
1660 | },
1661 | "require-dev": {
1662 | "nette/tester": "^2.0",
1663 | "tracy/tracy": "^2.3"
1664 | },
1665 | "type": "library",
1666 | "extra": {
1667 | "branch-alias": {
1668 | "dev-master": "3.0-dev"
1669 | }
1670 | },
1671 | "autoload": {
1672 | "classmap": [
1673 | "src/"
1674 | ]
1675 | },
1676 | "notification-url": "https://packagist.org/downloads/",
1677 | "license": [
1678 | "BSD-3-Clause",
1679 | "GPL-2.0",
1680 | "GPL-3.0"
1681 | ],
1682 | "authors": [
1683 | {
1684 | "name": "David Grudl",
1685 | "homepage": "https://davidgrudl.com"
1686 | },
1687 | {
1688 | "name": "Nette Community",
1689 | "homepage": "https://nette.org/contributors"
1690 | }
1691 | ],
1692 | "description": "🍀 Nette RobotLoader: high performance and comfortable autoloader that will search and autoload classes within your application.",
1693 | "homepage": "https://nette.org",
1694 | "keywords": [
1695 | "autoload",
1696 | "class",
1697 | "interface",
1698 | "nette",
1699 | "trait"
1700 | ],
1701 | "time": "2017-07-18T00:09:56+00:00"
1702 | },
1703 | {
1704 | "name": "nette/utils",
1705 | "version": "v2.4.8",
1706 | "source": {
1707 | "type": "git",
1708 | "url": "https://github.com/nette/utils.git",
1709 | "reference": "f1584033b5af945b470533b466b81a789d532034"
1710 | },
1711 | "dist": {
1712 | "type": "zip",
1713 | "url": "https://api.github.com/repos/nette/utils/zipball/f1584033b5af945b470533b466b81a789d532034",
1714 | "reference": "f1584033b5af945b470533b466b81a789d532034",
1715 | "shasum": ""
1716 | },
1717 | "require": {
1718 | "php": ">=5.6.0"
1719 | },
1720 | "conflict": {
1721 | "nette/nette": "<2.2"
1722 | },
1723 | "require-dev": {
1724 | "nette/tester": "~2.0",
1725 | "tracy/tracy": "^2.3"
1726 | },
1727 | "suggest": {
1728 | "ext-gd": "to use Image",
1729 | "ext-iconv": "to use Strings::webalize() and toAscii()",
1730 | "ext-intl": "for script transliteration in Strings::webalize() and toAscii()",
1731 | "ext-json": "to use Nette\\Utils\\Json",
1732 | "ext-mbstring": "to use Strings::lower() etc...",
1733 | "ext-xml": "to use Strings::length() etc. when mbstring is not available"
1734 | },
1735 | "type": "library",
1736 | "extra": {
1737 | "branch-alias": {
1738 | "dev-master": "2.4-dev"
1739 | }
1740 | },
1741 | "autoload": {
1742 | "classmap": [
1743 | "src/"
1744 | ]
1745 | },
1746 | "notification-url": "https://packagist.org/downloads/",
1747 | "license": [
1748 | "BSD-3-Clause",
1749 | "GPL-2.0",
1750 | "GPL-3.0"
1751 | ],
1752 | "authors": [
1753 | {
1754 | "name": "David Grudl",
1755 | "homepage": "https://davidgrudl.com"
1756 | },
1757 | {
1758 | "name": "Nette Community",
1759 | "homepage": "https://nette.org/contributors"
1760 | }
1761 | ],
1762 | "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.",
1763 | "homepage": "https://nette.org",
1764 | "keywords": [
1765 | "array",
1766 | "core",
1767 | "datetime",
1768 | "images",
1769 | "json",
1770 | "nette",
1771 | "paginator",
1772 | "password",
1773 | "slugify",
1774 | "string",
1775 | "unicode",
1776 | "utf-8",
1777 | "utility",
1778 | "validation"
1779 | ],
1780 | "time": "2017-08-20T17:32:29+00:00"
1781 | },
1782 | {
1783 | "name": "nikic/php-parser",
1784 | "version": "v3.1.2",
1785 | "source": {
1786 | "type": "git",
1787 | "url": "https://github.com/nikic/PHP-Parser.git",
1788 | "reference": "08131e7ff29de6bb9f12275c7d35df71f25f4d89"
1789 | },
1790 | "dist": {
1791 | "type": "zip",
1792 | "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/08131e7ff29de6bb9f12275c7d35df71f25f4d89",
1793 | "reference": "08131e7ff29de6bb9f12275c7d35df71f25f4d89",
1794 | "shasum": ""
1795 | },
1796 | "require": {
1797 | "ext-tokenizer": "*",
1798 | "php": ">=5.5"
1799 | },
1800 | "require-dev": {
1801 | "phpunit/phpunit": "~4.0|~5.0"
1802 | },
1803 | "bin": [
1804 | "bin/php-parse"
1805 | ],
1806 | "type": "library",
1807 | "extra": {
1808 | "branch-alias": {
1809 | "dev-master": "3.0-dev"
1810 | }
1811 | },
1812 | "autoload": {
1813 | "psr-4": {
1814 | "PhpParser\\": "lib/PhpParser"
1815 | }
1816 | },
1817 | "notification-url": "https://packagist.org/downloads/",
1818 | "license": [
1819 | "BSD-3-Clause"
1820 | ],
1821 | "authors": [
1822 | {
1823 | "name": "Nikita Popov"
1824 | }
1825 | ],
1826 | "description": "A PHP parser written in PHP",
1827 | "keywords": [
1828 | "parser",
1829 | "php"
1830 | ],
1831 | "time": "2017-11-04T11:48:34+00:00"
1832 | },
1833 | {
1834 | "name": "phpstan/phpstan",
1835 | "version": "0.7",
1836 | "source": {
1837 | "type": "git",
1838 | "url": "https://github.com/phpstan/phpstan.git",
1839 | "reference": "8da03c084b2c8e4a92d48f6926f6191c2c7783ad"
1840 | },
1841 | "dist": {
1842 | "type": "zip",
1843 | "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8da03c084b2c8e4a92d48f6926f6191c2c7783ad",
1844 | "reference": "8da03c084b2c8e4a92d48f6926f6191c2c7783ad",
1845 | "shasum": ""
1846 | },
1847 | "require": {
1848 | "nette/bootstrap": "^2.4 || ^3.0",
1849 | "nette/caching": "^2.4 || ^3.0",
1850 | "nette/di": "^2.4 || ^3.0",
1851 | "nette/robot-loader": "^2.4.2 || ^3.0",
1852 | "nette/utils": "^2.4 || ^3.0",
1853 | "nikic/php-parser": "^2.1 || ^3.0.2",
1854 | "php": "~7.0",
1855 | "symfony/console": "~2.7 || ~3.0",
1856 | "symfony/finder": "~2.7 || ~3.0"
1857 | },
1858 | "require-dev": {
1859 | "consistence/coding-standard": "~0.13.0",
1860 | "jakub-onderka/php-parallel-lint": "^0.9.2",
1861 | "phing/phing": "^2.16.0",
1862 | "phpunit/phpunit": "^6.0.7",
1863 | "satooshi/php-coveralls": "^1.0",
1864 | "slevomat/coding-standard": "^2.0"
1865 | },
1866 | "bin": [
1867 | "bin/phpstan"
1868 | ],
1869 | "type": "library",
1870 | "extra": {
1871 | "branch-alias": {
1872 | "dev-master": "0.7-dev"
1873 | }
1874 | },
1875 | "autoload": {
1876 | "psr-4": {
1877 | "PHPStan\\": "src/"
1878 | }
1879 | },
1880 | "notification-url": "https://packagist.org/downloads/",
1881 | "license": [
1882 | "MIT"
1883 | ],
1884 | "description": "PHPStan - PHP Static Analysis Tool",
1885 | "time": "2017-05-14T20:37:59+00:00"
1886 | },
1887 | {
1888 | "name": "squizlabs/php_codesniffer",
1889 | "version": "3.1.1",
1890 | "source": {
1891 | "type": "git",
1892 | "url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
1893 | "reference": "d667e245d5dcd4d7bf80f26f2c947d476b66213e"
1894 | },
1895 | "dist": {
1896 | "type": "zip",
1897 | "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/d667e245d5dcd4d7bf80f26f2c947d476b66213e",
1898 | "reference": "d667e245d5dcd4d7bf80f26f2c947d476b66213e",
1899 | "shasum": ""
1900 | },
1901 | "require": {
1902 | "ext-simplexml": "*",
1903 | "ext-tokenizer": "*",
1904 | "ext-xmlwriter": "*",
1905 | "php": ">=5.4.0"
1906 | },
1907 | "require-dev": {
1908 | "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0"
1909 | },
1910 | "bin": [
1911 | "bin/phpcs",
1912 | "bin/phpcbf"
1913 | ],
1914 | "type": "library",
1915 | "extra": {
1916 | "branch-alias": {
1917 | "dev-master": "3.x-dev"
1918 | }
1919 | },
1920 | "notification-url": "https://packagist.org/downloads/",
1921 | "license": [
1922 | "BSD-3-Clause"
1923 | ],
1924 | "authors": [
1925 | {
1926 | "name": "Greg Sherwood",
1927 | "role": "lead"
1928 | }
1929 | ],
1930 | "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
1931 | "homepage": "http://www.squizlabs.com/php-codesniffer",
1932 | "keywords": [
1933 | "phpcs",
1934 | "standards"
1935 | ],
1936 | "time": "2017-10-16T22:40:25+00:00"
1937 | }
1938 | ],
1939 | "aliases": [],
1940 | "minimum-stability": "stable",
1941 | "stability-flags": [],
1942 | "prefer-stable": false,
1943 | "prefer-lowest": false,
1944 | "platform": {
1945 | "php": "^7.0"
1946 | },
1947 | "platform-dev": []
1948 | }
1949 |
--------------------------------------------------------------------------------
/flickr-cli.sublime-project:
--------------------------------------------------------------------------------
1 | {
2 | "folders":[
3 | {
4 | "path": ".",
5 | "name": "FlickrCLI",
6 | "folder_exclude_patterns": [ "vendor" ],
7 | "file_exclude_patterns": [ ]
8 | }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/phpcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | bin/flickr-cli
6 | src
7 |
8 | vendor/
9 |
10 |
--------------------------------------------------------------------------------
/src/TheFox/FlickrCli/Command/AlbumsCommand.php:
--------------------------------------------------------------------------------
1 | setName('albums');
15 | $this->setDescription('List Photosets.');
16 | }
17 |
18 | /**
19 | * @param InputInterface $input
20 | * @param OutputInterface $output
21 | * @return int
22 | */
23 | protected function execute(InputInterface $input, OutputInterface $output): int
24 | {
25 | parent::execute($input, $output);
26 |
27 | $apiService = $this->getApiService();
28 |
29 | $photosetTitles = $apiService->getPhotosetTitles();
30 | foreach ($photosetTitles as $photosetId => $photosetTitle) {
31 | pcntl_signal_dispatch();
32 | if ($this->getExit()) {
33 | break;
34 | }
35 |
36 | printf('%s' . "\n", $photosetTitle);
37 | }
38 |
39 | return $this->getExit();
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/TheFox/FlickrCli/Command/AuthCommand.php:
--------------------------------------------------------------------------------
1 | io = new SymfonyStyle($nullInput, $nullOutput);
40 | }
41 |
42 | protected function configure()
43 | {
44 | parent::configure();
45 |
46 | $this->setName('auth');
47 | $this->setDescription('Retrieve the Access Token for your Flickr application.');
48 |
49 | $msg = 'Request authorisation even if the Access Token has already been stored.';
50 | $this->addOption('force', 'f', InputOption::VALUE_NONE, $msg);
51 | }
52 |
53 | /**
54 | * @param InputInterface $input
55 | * @param OutputInterface $output
56 | */
57 | protected function setup(InputInterface $input, OutputInterface $output)
58 | {
59 | $this->setIsConfigFileRequired(false);
60 |
61 | parent::setup($input, $output);
62 |
63 | $this->setupIo();
64 | }
65 |
66 | private function setupIo()
67 | {
68 | $io = new SymfonyStyle($this->getInput(), $this->getOutput());
69 | $this->io = $io;
70 | }
71 |
72 | /**
73 | * @param InputInterface $input
74 | * @param OutputInterface $output
75 | * @return int
76 | */
77 | protected function execute(InputInterface $input, OutputInterface $output): int
78 | {
79 | $this->setup($input, $output);
80 |
81 | $configFilePath = $this->getConfigFilePath();
82 |
83 | // Get the config file, or create one.
84 | try {
85 | $config = $this->loadConfig();
86 | } catch (RuntimeException $exception) {
87 | $filesystem = new Filesystem();
88 | if ($filesystem->exists($configFilePath)) {
89 | throw $exception;
90 | }
91 |
92 | // If we couldn't get the config, ask for the basic config values and then try again.
93 | $this->io->writeln('Go to https://www.flickr.com/services/apps/create/apply/ to create a new API key.');
94 | $customerKey = $this->io->ask('Consumer key');
95 | $customerSecret = $this->io->ask('Consumer secret');
96 |
97 | $config = [
98 | 'flickr' => [
99 | 'consumer_key' => $customerKey,
100 | 'consumer_secret' => $customerSecret,
101 | ],
102 | ];
103 | $this->saveConfig($config);
104 |
105 | // Fetch again, to make sure it's saved correctly.
106 | $config = $this->loadConfig();
107 | }
108 |
109 | $hasToken = isset($config['flickr']['token']) && isset($config['flickr']['token_secret']);
110 | if (!$hasToken || $this->getInput()->hasOption('force') && $this->getInput()->getOption('force')) {
111 | $newConfig = $this->authenticate($configFilePath, $config['flickr']['consumer_key'], $config['flickr']['consumer_secret']);
112 |
113 | $config['flickr']['token'] = $newConfig['token'];
114 | $config['flickr']['token_secret'] = $newConfig['token_secret'];
115 |
116 | $this->io->success(sprintf('Saving config to %s', $configFilePath));
117 | $this->saveConfig($config);
118 | }
119 |
120 | // Now test the stored credentials.
121 | $metadata = new Metadata($config['flickr']['consumer_key'], $config['flickr']['consumer_secret']);
122 | $metadata->setOauthAccess($config['flickr']['token'], $config['flickr']['token_secret']);
123 |
124 | $factory = new ApiFactory($metadata, new RezzzaGuzzleAdapter());
125 |
126 | $this->io->text('Test Login');
127 | $xml = $factory->call('flickr.test.login');
128 |
129 | $attributes = $xml->attributes();
130 | $stat = (string)$attributes->stat;
131 |
132 | if (strtolower($stat) == 'ok') {
133 | $this->io->success('Test Login successful');
134 | } else {
135 | $this->io->text(sprintf('Status: %s', $stat));
136 | }
137 |
138 | return $this->getExit();
139 | }
140 |
141 | /**
142 | * Authenticate with Flickr.
143 | *
144 | * @param string $configPath The config filename.
145 | * @param string $customerKey
146 | * @param string $customerSecret
147 | * @return array
148 | */
149 | protected function authenticate(string $configPath, string $customerKey, string $customerSecret)
150 | {
151 | $storage = new Memory();
152 |
153 | // Out-of-band, i.e. no callback required for a CLI application.
154 | $credentials = new Credentials($customerKey, $customerSecret, 'oob');
155 |
156 | $streamClient = new GuzzleStreamClient();
157 | $signature = new Signature($credentials);
158 |
159 | $flickrService = new Flickr($credentials, $streamClient, $storage, $signature);
160 | $token = $flickrService->requestRequestToken();
161 | if (!$token) {
162 | throw new RuntimeException('Request RequestToken failed.');
163 | }
164 |
165 | $accessToken = $token->getAccessToken();
166 | if (!$accessToken) {
167 | throw new RuntimeException('Cannot get Access Token.');
168 | }
169 |
170 | $accessTokenSecret = $token->getAccessTokenSecret();
171 | if (!$accessTokenSecret) {
172 | throw new RuntimeException('Cannot get Access Token Secret.');
173 | }
174 |
175 | // Ask user for permissions.
176 | $permissions = $this->getPermissionType();
177 |
178 | $additionalParameters = [
179 | 'oauth_token' => $accessToken,
180 | 'perms' => $permissions,
181 | ];
182 | $url = $flickrService->getAuthorizationUri($additionalParameters);
183 |
184 | $this->io->writeln(sprintf("Go to this URL to authorize FlickrCLI:\n\n%s", $url));
185 |
186 | // Flickr says, at this point:
187 | // "You have successfully authorized the application XYZ to use your credentials.
188 | // You should now type this code into the application:"
189 | $question = 'Paste the 9-digit code (with or without hyphens) here:';
190 | $verifier = $this->io->ask($question, null, function ($code) {
191 | $newCode = preg_replace('/[^0-9]/', '', $code);
192 | return $newCode;
193 | });
194 |
195 | $token = $flickrService->requestAccessToken($token, $verifier, $accessTokenSecret);
196 | if (!$token) {
197 | throw new RuntimeException('Request AccessToken failed.');
198 | }
199 |
200 | $accessToken = $token->getAccessToken();
201 | $accessTokenSecret = $token->getAccessTokenSecret();
202 |
203 | $newConfig = [
204 | 'token' => $accessToken,
205 | 'token_secret' => $accessTokenSecret,
206 | ];
207 | return $newConfig;
208 | }
209 |
210 | /**
211 | * Ask the user if they want to authenticate with read, write, or delete permissions.
212 | *
213 | * @return string The permission, one of 'read', write', or 'delete'. Defaults to 'read'.
214 | */
215 | protected function getPermissionType(): string
216 | {
217 | $this->io->writeln('The permission you grant to FlickrCLI depends on what you want to do with it.');
218 |
219 | $question = 'Please select from the following three options';
220 | $choices = [
221 | 'read' => 'download photos',
222 | 'write' => 'upload photos',
223 | 'delete' => 'download and/or delete photos from Flickr',
224 | ];
225 |
226 | // Note that we're not currently setting a default here, because it is not yet possible
227 | // to set a non-numeric key as the default. https://github.com/symfony/symfony/issues/15032
228 | $permissions = $this->io->choice($question, $choices);
229 | return $permissions;
230 | }
231 | }
232 |
--------------------------------------------------------------------------------
/src/TheFox/FlickrCli/Command/DeleteCommand.php:
--------------------------------------------------------------------------------
1 | setName('delete');
18 | $this->setDescription('Delete Photosets.');
19 |
20 | $this->addArgument('photosets', InputArgument::IS_ARRAY, 'Photosets to use.');
21 | }
22 |
23 | /**
24 | * @param InputInterface $input
25 | * @param OutputInterface $output
26 | * @return int
27 | */
28 | protected function execute(InputInterface $input, OutputInterface $output): int
29 | {
30 | parent::execute($input, $output);
31 |
32 | $photosets = $input->getArgument('photosets');
33 |
34 | $apiService = $this->getApiService();
35 | $apiFactory = $apiService->getApiFactory();
36 |
37 | $photosetTitles = $apiService->getPhotosetTitles();
38 |
39 | $this->getLogger()->notice('[main] start deleting files');
40 | foreach ($photosetTitles as $photosetId => $photosetTitle) {
41 | pcntl_signal_dispatch();
42 | if ($this->getExit()) {
43 | break;
44 | }
45 |
46 | if (!in_array($photosetTitle, $photosets)) {
47 | continue;
48 | }
49 |
50 | $xmlPhotoListOptions = [
51 | 'photoset_id' => $photosetId,
52 | ];
53 | $xmlPhotoList = $apiFactory->call('flickr.photosets.getPhotos', $xmlPhotoListOptions);
54 | $xmlPhotoListAttributes = $xmlPhotoList->photoset->attributes();
55 | $xmlPhotoListPagesTotal = (int)$xmlPhotoListAttributes->pages;
56 | $xmlPhotoListPhotosTotal = (int)$xmlPhotoListAttributes->total;
57 |
58 | $this->getLogger()->info(sprintf('[photoset] %s: %s', $photosetTitle, $xmlPhotoListPhotosTotal));
59 |
60 | $fileCount = 0;
61 | for ($page = 1; $page <= $xmlPhotoListPagesTotal; $page++) {
62 | pcntl_signal_dispatch();
63 | if ($this->getExit()) {
64 | break;
65 | }
66 |
67 | if ($page > 1) {
68 | $xmlPhotoListOptions['page'] = $page;
69 | $xmlPhotoList = $apiFactory->call('flickr.photosets.getPhotos', $xmlPhotoListOptions);
70 | }
71 |
72 | /**
73 | * @var int $n
74 | * @var SimpleXMLElement $photo
75 | */
76 | foreach ($xmlPhotoList->photoset->photo as $n => $photo) {
77 | pcntl_signal_dispatch();
78 | if ($this->getExit()) {
79 | break;
80 | }
81 |
82 | $fileCount++;
83 | $id = (string)$photo->attributes()->id;
84 | try {
85 | $apiFactory->call('flickr.photos.delete', ['photo_id' => $id]);
86 | $this->getLogger()->info(sprintf('[photo] %d/%d deleted %s', $page, $fileCount, $id));
87 | } catch (Exception $e) {
88 | $this->getLogger()->info(sprintf('[photo] %d/%d delete %s FAILED: %s', $page, $fileCount, $id, $e->getMessage()));
89 | }
90 | }
91 | }
92 | }
93 |
94 | return $this->getExit();
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/TheFox/FlickrCli/Command/DownloadCommand.php:
--------------------------------------------------------------------------------
1 | setName('download');
64 | $this->setDescription('Download files from Flickr.');
65 |
66 | $this->addOption('destination', 'd', InputOption::VALUE_OPTIONAL, 'Path to save files. Default: photosets');
67 |
68 | $idDirsDescr = 'Save downloaded files into ID-based directories. Default is to group by Album titles instead.';
69 | $this->addOption('id-dirs', 'i', InputOption::VALUE_NONE, $idDirsDescr);
70 |
71 | $forceDescr = 'Force Flickr CLI to download photos even if they already exist locally. ';
72 | $forceDescr .= 'Default is to skip existing downloads.';
73 | $this->addOption('force', 'f', InputOption::VALUE_NONE, $forceDescr);
74 |
75 | $this->addArgument('photosets', InputArgument::IS_ARRAY, 'Photosets to download.');
76 |
77 | $this->destinationPath = 'photosets';
78 | }
79 |
80 | private function setupDestination()
81 | {
82 | $filesystem = new Filesystem();
83 |
84 | // Destination directory. Default to 'photosets'.
85 | $customDestDir = $this->getInput()->getOption('destination');
86 | if (!empty($customDestDir)) {
87 | $this->destinationPath = rtrim($customDestDir, '/');
88 | }
89 | if (!$filesystem->exists($this->destinationPath)) {
90 | $filesystem->mkdir($this->destinationPath, 0755);
91 | }
92 | }
93 |
94 | /**
95 | * Executes the download command.
96 | *
97 | * @param InputInterface $input An InputInterface instance
98 | * @param OutputInterface $output An OutputInterface instance
99 | * @return int 0 if everything went fine, or an error code.
100 | */
101 | protected function execute(InputInterface $input, OutputInterface $output): int
102 | {
103 | parent::execute($input, $output);
104 |
105 | $this->setupDestination();
106 |
107 | // Force download?
108 | $this->forceDownload = $input->getOption('force');
109 |
110 | // Run the actual download.
111 | if ($input->getOption('id-dirs')) {
112 | // If downloaded files should be saved into download-dir/hash/hash/photo-id/ directories.
113 | $exit = $this->downloadById();
114 | } else {
115 | // If download directories should match Album titles.
116 | $exit = $this->downloadByAlbumTitle();
117 | }
118 |
119 | return $exit;
120 | }
121 |
122 | /**
123 | * Download photos to directories named after the album (i.e. photoset, in the original parlance).
124 | *
125 | * @return int
126 | */
127 | protected function downloadByAlbumTitle(): int
128 | {
129 | $this->getLogger()->info(sprintf('Downloading to Album-based directories in: %s', $this->destinationPath));
130 |
131 | $apiService = $this->getApiService();
132 | $apiFactory = $apiService->getApiFactory();
133 | $xml = $apiFactory->call('flickr.photosets.getList');
134 |
135 | $photosets = $this->getInput()->getArgument('photosets');
136 | if (!is_array($photosets)) {
137 | throw new RuntimeException('photosets is not an array');
138 | }
139 |
140 | $photosetsInUse = [];
141 | if (count($photosets)) {
142 | $photosetTitles = $apiService->getPhotosetTitles();
143 |
144 | foreach ($photosets as $argPhotosetTitle) {
145 | pcntl_signal_dispatch();
146 | if ($this->getExit()) {
147 | break;
148 | }
149 |
150 | if (!in_array($argPhotosetTitle, $photosetTitles)) {
151 | continue;
152 | }
153 |
154 | $photosetsInUse[] = $argPhotosetTitle;
155 | }
156 |
157 | foreach ($photosets as $argPhotosetTitle) {
158 | pcntl_signal_dispatch();
159 | if ($this->getExit()) {
160 | break;
161 | }
162 |
163 | if (in_array($argPhotosetTitle, $photosetsInUse)) {
164 | continue;
165 | }
166 |
167 | foreach ($photosetTitles as $photosetTitle) {
168 | if (!fnmatch($argPhotosetTitle, $photosetTitle)) {
169 | continue;
170 | }
171 |
172 | $photosetsInUse[] = $photosetTitle;
173 | }
174 | }
175 | } else {
176 | foreach ($xml->photosets->photoset as $photoset) {
177 | pcntl_signal_dispatch();
178 | if ($this->getExit()) {
179 | break;
180 | }
181 |
182 | $photosetsInUse[] = $photoset->title;
183 | }
184 | }
185 |
186 | $filesystem = new Filesystem();
187 | $totalDownloaded = 0;
188 | $totalFiles = 0;
189 |
190 | /** @var $photoset SimpleXMLElement */
191 | foreach ($xml->photosets->photoset as $photoset) {
192 | pcntl_signal_dispatch();
193 | if ($this->getExit()) {
194 | break;
195 | }
196 |
197 | if (!in_array($photoset->title, $photosetsInUse)) {
198 | continue;
199 | }
200 |
201 | $photosetId = (int)$photoset->attributes()->id;
202 | $photosetTitle = (string)$photoset->title;
203 | $this->getLogger()->info(sprintf('[photoset] %s', $photosetTitle));
204 |
205 | $destinationPath = sprintf('%s/%s', $this->destinationPath, $photosetTitle);
206 |
207 | if (!$filesystem->exists($destinationPath)) {
208 | $this->getLogger()->info(sprintf('[dir] create: %s', $destinationPath));
209 | $filesystem->mkdir($destinationPath);
210 | }
211 |
212 | $this->getLogger()->info(sprintf('[photoset] %s: get photo list', $photosetTitle));
213 | $xmlPhotoList = $apiFactory->call('flickr.photosets.getPhotos', [
214 | 'photoset_id' => $photosetId,
215 | ]);
216 | $xmlPhotoListPagesTotal = (int)$xmlPhotoList->photoset->attributes()->pages;
217 | // $xmlPhotoListPhotosTotal = (int)$xmlPhotoList->photoset->attributes()->total;
218 |
219 | $fileCount = 0;
220 |
221 | for ($page = 1; $page <= $xmlPhotoListPagesTotal; $page++) {
222 | pcntl_signal_dispatch();
223 | if ($this->getExit()) {
224 | break;
225 | }
226 |
227 | $this->getLogger()->info(sprintf('[page] %d', $page));
228 |
229 | if ($page > 1) {
230 | $this->getLogger()->info(sprintf('[photoset] %s: get photo list', $photosetTitle));
231 | $xmlPhotoList = $apiFactory->call('flickr.photosets.getPhotos', [
232 | 'photoset_id' => $photosetId,
233 | 'page' => $page,
234 | ]);
235 | }
236 |
237 | /** @var $photo SimpleXMLElement */
238 | foreach ($xmlPhotoList->photoset->photo as $photo) {
239 | pcntl_signal_dispatch();
240 | if ($this->getExit()) {
241 | break;
242 | }
243 |
244 | $this->getLogger()->debug(sprintf('[media] %d/%d photo %s', $page, $fileCount, $photo['id']));
245 | $downloaded = $this->downloadPhoto($photo, $destinationPath);
246 | if ($downloaded && isset($downloaded->filesize)) {
247 | $totalDownloaded += $downloaded->filesize;
248 | }
249 | $fileCount++;
250 | }
251 | }
252 | }
253 |
254 | if ($totalDownloaded > 0) {
255 | $bytesize = new ByteSize();
256 | $totalDownloadedMsg = $bytesize->format($totalDownloaded);
257 | } else {
258 | $totalDownloadedMsg = 0;
259 | }
260 |
261 | $this->getLogger()->info(sprintf('[main] total downloaded: %d', $totalDownloadedMsg));
262 | $this->getLogger()->info(sprintf('[main] total files: %d', $totalFiles));
263 | $this->getLogger()->info('[main] exit');
264 |
265 | return $this->getExit();
266 | }
267 |
268 | /**
269 | * Download a single given photo from Flickr. Won't be downloaded if already exists locally; if it is downloaded the
270 | * additional 'filesize' property will be set on the return element.
271 | *
272 | * @param SimpleXMLElement $photo
273 | * @param string $destinationPath
274 | * @param string $basename The filename to save the downloaded file to (without extension).
275 | * @return SimpleXMLElement|boolean Photo metadata as returned by Flickr, or false if something went wrong.
276 | * @throws Exception
277 | */
278 | private function downloadPhoto(SimpleXMLElement $photo, string $destinationPath, string $basename = null)
279 | {
280 | $id = (string)$photo->attributes()->id;
281 |
282 | $apiFactory = $this->getApiService()->getApiFactory();
283 |
284 | try {
285 | $xmlPhoto = $apiFactory->call('flickr.photos.getInfo', [
286 | 'photo_id' => $id,
287 | 'secret' => (string)$photo->attributes()->secret,
288 | ]);
289 | if (!$xmlPhoto) {
290 | return false;
291 | }
292 | } catch (Exception $e) {
293 | $this->getLogger()->error(sprintf(
294 | '%s, GETINFO FAILED: %s',
295 | $id,
296 | $e->getMessage()
297 | ))
298 | ;
299 |
300 | return false;
301 | }
302 |
303 | if (isset($xmlPhoto->photo->title) && (string)$xmlPhoto->photo->title) {
304 | $title = (string)$xmlPhoto->photo->title;
305 | } else {
306 | $title = '';
307 | }
308 |
309 | $server = (string)$xmlPhoto->photo->attributes()->server;
310 | $farm = (string)$xmlPhoto->photo->attributes()->farm;
311 | $originalSecret = (string)$xmlPhoto->photo->attributes()->originalsecret;
312 | $originalFormat = (string)$xmlPhoto->photo->attributes()->originalformat;
313 | $description = (string)$xmlPhoto->photo->description;
314 | $media = (string)$xmlPhoto->photo->attributes()->media;
315 | //$ownerPathalias = (string)$xmlPhoto->photo->owner->attributes()->path_alias;
316 | //$ownerNsid = (string)$xmlPhoto->photo->owner->attributes()->nsid;
317 |
318 | // Set the filename.
319 | if (empty($basename)) {
320 | $fileName = sprintf('%s.%s', $title ? $title : $id, $originalFormat);
321 | } else {
322 | $fileName = sprintf('%s.%s', $basename, $originalFormat);
323 | }
324 | $filePath = sprintf('%s/%s', rtrim($destinationPath, '/'), $fileName);
325 | $filePathTmp = sprintf('%s/%s.%s.tmp', $destinationPath, $id, $originalFormat);
326 |
327 | $filesystem = new Filesystem();
328 | if ($filesystem->exists($filePath) && !$this->forceDownload) {
329 | $this->getLogger()->debug(sprintf('File %s already downloaded to %s', $id, $filePath));
330 |
331 | /** @var SimpleXMLElement $photo */
332 | $photo = $xmlPhoto->photo;
333 |
334 | return $photo;
335 | }
336 |
337 | // URL format for the original image. See https://www.flickr.com/services/api/misc.urls.html
338 | // https://farm{farm-id}.staticflickr.com/{server-id}/{id}_{o-secret}_o.(jpg|gif|png)
339 | $urlFormat = 'https://farm%s.staticflickr.com/%s/%s_%s_o.%s';
340 | $url = sprintf($urlFormat, $farm, $server, $id, $originalSecret, $originalFormat);
341 |
342 | if ($media == 'video') {
343 | // $url = 'http://www.flickr.com/photos/'.$ownerPathalias.'/'.$id.'/play/orig/'.$originalSecret.'/';
344 | // $url = 'https://www.flickr.com/video_download.gne?id='.$id;
345 |
346 | // $contentDispositionHeaderArray = array();
347 |
348 | // try{
349 | // $client = new GuzzleHttpClient();
350 | // $request = $client->head($url);
351 | // $response = $request->send();
352 |
353 | // $url = $response->getEffectiveUrl();
354 |
355 | // $contentDispositionHeader = $response->getHeader('content-disposition');
356 | // $contentDispositionHeaderArray = $contentDispositionHeader->toArray();
357 | // }
358 | // catch(Exception $e){
359 | // $this->log->info(sprintf('[%s] %s, farm %s, server %s, %s HEAD FAILED: %s',
360 | // $media, $id, $farm, $server, $fileName, $e->getMessage()));
361 | // $this->logFilesFailed->error($id.'.'.$originalFormat);
362 |
363 | // continue;
364 | // }
365 |
366 | // if(count($contentDispositionHeaderArray)){
367 | // $pos = strpos(strtolower($contentDispositionHeaderArray[0]), 'filename=');
368 | // if($pos !== false){
369 | // $pathinfo = pathinfo(substr($contentDispositionHeaderArray[0], $pos + 9));
370 | // if(isset($pathinfo['extension'])){
371 | // $originalFormat = $pathinfo['extension'];
372 | // $fileName = ($title ? $title : $id).'.'.$originalFormat;
373 | // $filePath = $dstDirFullPath.'/'.$fileName;
374 | // $filePathTmp = $dstDirFullPath.'/'.$id.'.'.$originalFormat.'.tmp';
375 |
376 | // if($filesystem->exists($filePath)){
377 | // continue;
378 | // }
379 | // }
380 | // }
381 | // }
382 |
383 | $this->getLogger()->error('video not supported yet');
384 | //$this->loggerFilesFailed->error($id . ': video not supported yet');
385 | return false;
386 | }
387 |
388 | $client = new GuzzleHttpClient($url);
389 |
390 | $streamRequestFactory = new PhpStreamRequestFactory();
391 | try {
392 | $request = $client->get();
393 | $stream = $streamRequestFactory->fromRequest($request);
394 | } catch (Exception $e) {
395 | $this->getLogger()->error(sprintf(
396 | '[%s] %s, farm %s, server %s, %s FAILED: %s',
397 | $media,
398 | $id,
399 | $farm,
400 | $server,
401 | $fileName,
402 | $e->getMessage()
403 | ))
404 | ;
405 | //$this->loggerFilesFailed->error($id . '.' . $originalFormat);
406 |
407 | return false;
408 | }
409 |
410 | $size = $stream->getSize();
411 | if (false !== $size) {
412 | $bytesize = new ByteSize();
413 | $sizeStr = $bytesize->format((int)$size);
414 | } else {
415 | $sizeStr = 'N/A';
416 | }
417 |
418 | $this->getLogger()->info(sprintf(
419 | "[%s] %s, farm %s, server %s, %s, '%s', %s",
420 | $media,
421 | $id,
422 | $farm,
423 | $server,
424 | $fileName,
425 | $description,
426 | $sizeStr
427 | ))
428 | ;
429 |
430 | $timePrev = time();
431 | $downloaded = 0;
432 | $downloadedPrev = 0;
433 | $downloadedDiff = 0;
434 |
435 | $fh = fopen($filePathTmp, 'wb');
436 | if (false === $fh) {
437 | throw new RuntimeException(sprintf('Unable to open %s for writing.', $filePathTmp));
438 | }
439 | while (!$stream->feof()) {
440 | pcntl_signal_dispatch();
441 | if ($this->getExit()) {
442 | break;
443 | }
444 |
445 | $data = $stream->read(FlickrCli::DOWNLOAD_STREAM_READ_LEN);
446 | $dataLen = strlen($data);
447 | fwrite($fh, $data);
448 |
449 | $downloaded += $dataLen;
450 |
451 | if ($size !== false) {
452 | $percent = $downloaded / $size * 100;
453 | } else {
454 | $percent = 0;
455 | }
456 | if ($percent > 100) {
457 | $percent = 100;
458 | }
459 |
460 | $progressbarDownloaded = round($percent / 100 * FlickrCli::DOWNLOAD_PROGRESSBAR_ITEMS);
461 | $progressbarRest = FlickrCli::DOWNLOAD_PROGRESSBAR_ITEMS - $progressbarDownloaded;
462 |
463 | $timeCur = time();
464 | if ($timeCur != $timePrev) {
465 | $timePrev = $timeCur;
466 | $downloadedDiff = $downloaded - $downloadedPrev;
467 | $downloadedPrev = $downloaded;
468 | }
469 |
470 | $downloadedDiffStr = '';
471 | if ($downloadedDiff) {
472 | $bytesize = new ByteSize();
473 | $downloadedDiffStr = $bytesize->format($downloadedDiff) . '/s';
474 | }
475 |
476 | if ($size !== false) {
477 | // If we know the stream size, show a progress bar.
478 | printf(
479 | "[file] %6.2f%% [%s%s] %s %10s\x1b[0K\r",
480 | $percent,
481 | str_repeat('#', $progressbarDownloaded),
482 | str_repeat(' ', $progressbarRest),
483 | number_format($downloaded),
484 | $downloadedDiffStr
485 | );
486 | } else {
487 | // Otherwise, just show the amount downloaded and speed.
488 | printf("[file] %s %10s\x1b[0K\r", number_format($downloaded), $downloadedDiffStr);
489 | }
490 | }
491 | fclose($fh);
492 | print "\n";
493 |
494 | $fileTmpSize = filesize($filePathTmp);
495 |
496 | if ($this->getExit()) {
497 | $filesystem->remove($filePathTmp);
498 | } elseif (($size && $fileTmpSize != $size) || $fileTmpSize <= 1024) {
499 | $filesystem->remove($filePathTmp);
500 |
501 | $this->getLogger()->error(sprintf('[%s] %s FAILED: temp file size wrong: %d', $media, $id, $fileTmpSize));
502 | } else {
503 | // Rename to its final destination, and return the photo metadata.
504 | $filesystem->rename($filePathTmp, $filePath, $this->forceDownload);
505 | $xmlPhoto->photo->filesize = $size;
506 |
507 | /** @var SimpleXMLElement $photo */
508 | $photo = $xmlPhoto->photo;
509 |
510 | return $photo;
511 | }
512 |
513 | return false;
514 | }
515 |
516 | /**
517 | * Download all photos, whether in a set/album or not, into directories named by photo ID.
518 | */
519 | private function downloadById()
520 | {
521 | $this->getLogger()->info(sprintf('Downloading to ID-based directories in: %s', $this->destinationPath));
522 |
523 | $apiFactory = $this->getApiService()->getApiFactory();
524 |
525 | // 1. Download any photos not in a set.
526 | $notInSetPage = 1;
527 | do {
528 | $notInSet = $apiFactory->call('flickr.photos.getNotInSet', ['page' => $notInSetPage]);
529 | $pages = (int)$notInSet->photos['pages'];
530 | $this->getLogger()->info(sprintf('Not in set p%s/%d', $notInSetPage, $pages));
531 |
532 | $notInSetPage++;
533 | foreach ($notInSet->photos->photo as $photo) {
534 | $this->downloadPhotoById($photo);
535 | }
536 | } while ($notInSetPage <= $notInSet->photos['pages']);
537 |
538 | // 2. Download all photos in all sets.
539 | $setsPage = 1;
540 | do {
541 | $sets = $apiFactory->call('flickr.photosets.getList', ['page' => $setsPage]);
542 | $pages = (int)$sets->photosets['pages'];
543 | $this->getLogger()->info(sprintf('Sets p%d/%d', $setsPage, $pages));
544 |
545 | foreach ($sets->photosets->photoset as $set) {
546 | // Loop through all pages in this set.
547 | $setPhotosPage = 1;
548 | do {
549 | $params = [
550 | 'photoset_id' => $set['id'],
551 | 'page' => $setPhotosPage,
552 | ];
553 | $setPhotos = $apiFactory->call('flickr.photosets.getPhotos', $params);
554 |
555 | $title = (string)$set->title;
556 | $total = (int)$setPhotos->photoset['total'];
557 | $setPages = (int)$setPhotos->photoset['pages'];
558 |
559 | $this->getLogger()->info(sprintf(
560 | '[Set %s] %s photos (p%s/%s)',
561 | $title,
562 | $total,
563 | $setPhotosPage,
564 | $setPages
565 | ))
566 | ;
567 | foreach ($setPhotos->photoset->photo as $photo) {
568 | $this->downloadPhotoById($photo);
569 | }
570 | $setPhotosPage++;
571 | } while ($setPhotosPage <= $setPhotos->photos['pages']);
572 | }
573 | $setsPage++;
574 | } while ($setsPage <= (int)$sets->photosets['pages']);
575 |
576 | return $this->getExit();
577 | }
578 |
579 | /**
580 | * Download a single photo.
581 | *
582 | * @param SimpleXMLElement $photo Basic photo metadata.
583 | */
584 | private function downloadPhotoById(SimpleXMLElement $photo)
585 | {
586 | $id = $photo['id'];
587 | $idHash = md5($id);
588 | $destinationPath = sprintf('%s/%s/%s/%s/%s/%s', $this->destinationPath, $idHash[0], $idHash[1], $idHash[2], $idHash[3], $id);
589 |
590 | $filesystem = new Filesystem();
591 | if (!$filesystem->exists($destinationPath)) {
592 | $filesystem->mkdir($destinationPath, 0755);
593 | }
594 |
595 | // Save the actual file.
596 | $apiFactory = $this->getApiService()->getApiFactory();
597 | $photo = $this->downloadPhoto($photo, $destinationPath, $id);
598 | if (false === $photo) {
599 | $this->getLogger()->error(sprintf('Unable to get metadata about photo: %s', $id));
600 | return;
601 | }
602 |
603 | $fn = $this->getMappingFunction($apiFactory);
604 |
605 | $metadata = $fn($photo);
606 |
607 | $content = Yaml::dump($metadata);
608 | $filesystem->dumpFile(sprintf('%s/metadata.yml', $destinationPath), $content);
609 | }
610 |
611 | /**
612 | * @param ApiFactory $apiFactory
613 | * @return \Closure
614 | */
615 | private function getMappingFunction(ApiFactory $apiFactory)
616 | {
617 | /**
618 | * @param SimpleXMLElement $photo
619 | * @return array
620 | */
621 | $fn = function (SimpleXMLElement $photo) use ($apiFactory) {
622 | // Metadata
623 | $metadataFn = $this->getMetadataMappingFunction();
624 | $metadata = $metadataFn($photo);
625 |
626 | if (isset($photo->photo->description->_content)) {
627 | $metadata['description'] = (string)$photo->photo->description->_content;
628 | }
629 |
630 | // Tags
631 | if (isset($photo->tags->tag)) {
632 | //$tagsFn = $this->getTagMappingFunction();
633 | //$tags = (array)$photo->tags;
634 | //$metadata['tags'] = array_map($tagsFn, $tags);
635 |
636 | foreach ($photo->tags->tag as $tag) {
637 | $metadata['tags'][] = [
638 | 'id' => (string)$tag['id'],
639 | 'slug' => (string)$tag,
640 | 'title' => (string)$tag['raw'],
641 | 'machine' => $tag['machine_tag'] !== '0',
642 | ];
643 | }
644 | }
645 |
646 | // Location
647 | if (isset($photo->location)) {
648 | $metadata['location'] = [
649 | 'latitude' => (float)$photo->location['latitude'],
650 | 'longitude' => (float)$photo->location['longitude'],
651 | 'accuracy' => (integer)$photo->location['accuracy'],
652 | ];
653 | }
654 |
655 | // Contexts
656 | $contexts = $apiFactory->call('flickr.photos.getAllContexts', ['photo_id' => $photo['id']]);
657 | foreach ($contexts->set as $set) {
658 | $metadata['sets'][] = [
659 | 'id' => (string)$set['id'],
660 | 'title' => (string)$set['title'],
661 | ];
662 | }
663 |
664 | // Pools
665 | foreach ($contexts->pool as $pool) {
666 | $metadata['pools'][] = [
667 | 'id' => (string)$pool['id'],
668 | 'title' => (string)$pool['title'],
669 | 'url' => (string)$pool['url'],
670 | ];
671 | }
672 |
673 | return $metadata;
674 | };
675 |
676 | return $fn;
677 | }
678 |
679 | /**
680 | * @return \Closure
681 | */
682 | private function getMetadataMappingFunction()
683 | {
684 | /**
685 | * @param SimpleXMLElement $photo
686 | * @return array
687 | */
688 | $fn = function (SimpleXMLElement $photo) {
689 | $metadata = [
690 | 'id' => (int)$photo['id'],
691 | 'title' => (string)$photo->title,
692 | 'license' => (string)$photo['license'],
693 | 'safety_level' => (string)$photo['safety_level'],
694 | 'rotation' => (string)$photo['rotation'],
695 | 'media' => (string)$photo['media'],
696 | 'format' => (string)$photo['originalformat'],
697 | 'owner' => [
698 | 'nsid' => (string)$photo->owner['nsid'],
699 | 'username' => (string)$photo->owner['username'],
700 | 'realname' => (string)$photo->owner['realname'],
701 | 'path_alias' => (string)$photo->owner['path_alias'],
702 | ],
703 | 'visibility' => [
704 | 'ispublic' => (boolean)$photo->visibility['ispublic'],
705 | 'isfriend' => (boolean)$photo->visibility['isfriend'],
706 | 'isfamily' => (boolean)$photo->visibility['isfamily'],
707 | ],
708 | 'dates' => [
709 | 'posted' => (string)$photo->dates['posted'],
710 | 'taken' => (string)$photo->dates['taken'],
711 | 'takengranularity' => (int)$photo->dates['takengranularity'],
712 | 'takenunknown' => (string)$photo->dates['takenunknown'],
713 | 'lastupdate' => (string)$photo->dates['lastupdate'],
714 | 'uploaded' => (string)$photo['dateuploaded'],
715 | ],
716 | 'tags' => [],
717 | 'sets' => [],
718 | 'pools' => [],
719 | ];
720 | return $metadata;
721 | };
722 |
723 | return $fn;
724 | }
725 | }
726 |
--------------------------------------------------------------------------------
/src/TheFox/FlickrCli/Command/FilesCommand.php:
--------------------------------------------------------------------------------
1 | setName('files');
28 | $this->setDescription('List Files.');
29 |
30 | $this->addOption('config', 'c', InputOption::VALUE_OPTIONAL, 'Path to config file. Default: config.yml');
31 | $this->addArgument('photosets', InputArgument::IS_ARRAY, 'Photosets to use.');
32 | }
33 |
34 | /**
35 | * @param InputInterface $input
36 | * @param OutputInterface $output
37 | * @return int
38 | */
39 | protected function execute(InputInterface $input, OutputInterface $output): int
40 | {
41 | parent::execute($input, $output);
42 |
43 | $photosets = $input->getArgument('photosets');
44 |
45 | $apiService = $this->getApiService();
46 | $apiFactory = $apiService->getApiFactory();
47 |
48 | $photosetTitles = $apiService->getPhotosetTitles();
49 | foreach ($photosetTitles as $photosetId => $photosetTitle) {
50 | pcntl_signal_dispatch();
51 | if ($this->getExit()) {
52 | break;
53 | }
54 |
55 | if (!in_array($photosetTitle, $photosets)) {
56 | continue;
57 | }
58 |
59 | $xmlPhotoListOptions = ['photoset_id' => $photosetId];
60 | $xmlPhotoList = $apiFactory->call('flickr.photosets.getPhotos', $xmlPhotoListOptions);
61 | $xmlPhotoListPagesTotal = (int)$xmlPhotoList->photoset->attributes()->pages;
62 | $xmlPhotoListPhotosTotal = (int)$xmlPhotoList->photoset->attributes()->total;
63 |
64 | printf('%s (%d)' . "\n", $photosetTitle, $xmlPhotoListPhotosTotal);
65 |
66 | $fileCount = 0;
67 |
68 | for ($page = 1; $page <= $xmlPhotoListPagesTotal; $page++) {
69 | pcntl_signal_dispatch();
70 | if ($this->getExit()) {
71 | break;
72 | }
73 |
74 | if ($page > 1) {
75 | $xmlPhotoListOptions['page'] = $page;
76 | $xmlPhotoList = $apiFactory->call('flickr.photosets.getPhotos', $xmlPhotoListOptions);
77 | }
78 |
79 | /**
80 | * @var int $n
81 | * @var SimpleXMLElement $photo
82 | */
83 | foreach ($xmlPhotoList->photoset->photo as $n => $photo) {
84 | pcntl_signal_dispatch();
85 | if ($this->getExit()) {
86 | break;
87 | }
88 |
89 | $id = (string)$photo->attributes()->id;
90 | $fileCount++;
91 |
92 | printf(' %d/%d %s' . "\n", $page, $fileCount, $id);
93 | }
94 | }
95 | }
96 |
97 | return $this->getExit();
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/TheFox/FlickrCli/Command/FlickrCliCommand.php:
--------------------------------------------------------------------------------
1 | exit = 0;
74 | $this->logger = new NullLogger();
75 | $this->output = new NullOutput();
76 | $this->configFilePath = 'config.yml';
77 | $this->isConfigFileRequired = true;
78 | $this->config = [];
79 | }
80 |
81 | /**
82 | * @return int
83 | */
84 | public function getExit(): int
85 | {
86 | return $this->exit;
87 | }
88 |
89 | /**
90 | * @param int $exit
91 | */
92 | public function setExit(int $exit)
93 | {
94 | $this->exit = $exit;
95 | }
96 |
97 | /**
98 | * @return LoggerInterface
99 | */
100 | public function getLogger(): LoggerInterface
101 | {
102 | return $this->logger;
103 | }
104 |
105 | /**
106 | * @return InputInterface
107 | */
108 | public function getInput(): InputInterface
109 | {
110 | return $this->input;
111 | }
112 |
113 | /**
114 | * @return OutputInterface
115 | */
116 | public function getOutput(): OutputInterface
117 | {
118 | return $this->output;
119 | }
120 |
121 | /**
122 | * @return string
123 | */
124 | public function getConfigFilePath(): string
125 | {
126 | return $this->configFilePath;
127 | }
128 |
129 | /**
130 | * @return bool
131 | */
132 | public function isConfigFileRequired(): bool
133 | {
134 | return $this->isConfigFileRequired;
135 | }
136 |
137 | /**
138 | * @param bool $isConfigFileRequired
139 | */
140 | public function setIsConfigFileRequired(bool $isConfigFileRequired)
141 | {
142 | $this->isConfigFileRequired = $isConfigFileRequired;
143 | }
144 |
145 | /**
146 | * @return array
147 | */
148 | public function getConfig(): array
149 | {
150 | return $this->config;
151 | }
152 |
153 | /**
154 | * @param array $config
155 | */
156 | public function setConfig(array $config)
157 | {
158 | $this->config = $config;
159 | }
160 |
161 | /**
162 | * @return ApiService
163 | */
164 | public function getApiService(): ApiService
165 | {
166 | return $this->apiService;
167 | }
168 |
169 | /**
170 | * Configure the command.
171 | * This adds the standard 'config' and 'log' options that are common to all Flickr CLI commands.
172 | */
173 | protected function configure()
174 | {
175 | $this
176 | ->addOption('config', 'c', InputOption::VALUE_OPTIONAL, 'Path to config file. Default: ./config.yml')//->addOption('log', 'l', InputOption::VALUE_OPTIONAL, 'Path to log directory. Default: ./log')
177 | ;
178 | }
179 |
180 | /**
181 | * @param InputInterface $input
182 | * @param OutputInterface $output
183 | */
184 | protected function setup(InputInterface $input, OutputInterface $output)
185 | {
186 | $this->input = $input;
187 | $this->output = $output;
188 |
189 | $this->signalHandlerSetup();
190 | $this->setupLogger();
191 |
192 | $this->setupConfig();
193 |
194 | $this->setupServices();
195 | }
196 |
197 | private function setupLogger()
198 | {
199 | $this->logger = new Logger($this->getName());
200 |
201 | switch ($this->output->getVerbosity()) {
202 | case OutputInterface::VERBOSITY_DEBUG: // -vvv
203 | $logLevel = Logger::DEBUG;
204 | break;
205 |
206 | case OutputInterface::VERBOSITY_VERY_VERBOSE: // -vv
207 | $logLevel = Logger::INFO;
208 | break;
209 |
210 | case OutputInterface::VERBOSITY_VERBOSE: // -v
211 | $logLevel = Logger::NOTICE;
212 | break;
213 |
214 | case OutputInterface::VERBOSITY_QUIET:
215 | $logLevel = Logger::ERROR;
216 | break;
217 |
218 | case OutputInterface::VERBOSITY_NORMAL:
219 | default:
220 | $logLevel = Logger::WARNING;
221 | }
222 |
223 | //$logFormatter = new LineFormatter("[%datetime%] %level_name%: %message%\n");
224 |
225 | $handler = new StreamHandler('php://stdout', $logLevel);
226 | //$handler->setFormatter($logFormatter);
227 | $this->logger->pushHandler($handler);
228 | }
229 |
230 | private function setupConfig()
231 | {
232 | $input = $this->getInput();
233 |
234 | if ($input->hasOption('config') && $input->getOption('config')) {
235 | $configFilePath = $input->getOption('config');
236 | } elseif ($envConfigFile = getenv('FLICKRCLI_CONFIG')) {
237 | $configFilePath = $envConfigFile;
238 | }
239 |
240 | if (!isset($configFilePath) || !$configFilePath) {
241 | throw new RuntimeException('No config file path found.');
242 | }
243 | $this->configFilePath = $configFilePath;
244 |
245 | $filesystem = new Filesystem();
246 | if ($filesystem->exists($this->configFilePath)) {
247 | $this->loadConfig();
248 | } elseif ($this->isConfigFileRequired()) {
249 | throw new RuntimeException(sprintf('Config file not found: %s', $this->configFilePath));
250 | }
251 | }
252 |
253 | /**
254 | * @return array
255 | */
256 | public function loadConfig(): array
257 | {
258 | $configFilePath = $this->getConfigFilePath();
259 | if (!$configFilePath) {
260 | throw new RuntimeException('Config File Path is not set.');
261 | }
262 |
263 | $this->getLogger()->debug(sprintf('Load configuration: %s', $this->getConfigFilePath()));
264 |
265 | /** @var string[][] $config */
266 | $config = Yaml::parse($configFilePath);
267 |
268 | if (!isset($config)
269 | || !isset($config['flickr'])
270 | || !isset($config['flickr']['consumer_key'])
271 | || !isset($config['flickr']['consumer_secret'])
272 | ) {
273 | throw new RuntimeException('Invalid configuration file.');
274 | }
275 |
276 | $this->config = $config;
277 | return $this->config;
278 | }
279 |
280 | /**
281 | * @param array|null $config
282 | */
283 | public function saveConfig(array $config = null)
284 | {
285 | if ($config) {
286 | $this->setConfig($config);
287 | } else {
288 | $config = $this->getConfig();
289 | }
290 |
291 | $configContent = Yaml::dump($config);
292 |
293 | $configFilePath = $this->getConfigFilePath();
294 |
295 | $filesystem = new Filesystem();
296 | $filesystem->touch($configFilePath);
297 | $filesystem->chmod($configFilePath, 0600);
298 | $filesystem->dumpFile($configFilePath, $configContent);
299 | }
300 |
301 | /**
302 | * @return bool
303 | */
304 | private function setupServices()
305 | {
306 | $config = $this->getConfig();
307 | if (!$config) {
308 | return false;
309 | }
310 |
311 | if (!array_key_exists('flickr', $config)) {
312 | return false;
313 | }
314 |
315 | $consumerKey = $config['flickr']['consumer_key'];
316 | if (!$consumerKey) {
317 | return false;
318 | }
319 |
320 | $consumerSecret = $config['flickr']['consumer_secret'];
321 | if (!$consumerSecret) {
322 | return false;
323 | }
324 |
325 | $token = $config['flickr']['token'];
326 | if (!$token) {
327 | return false;
328 | }
329 |
330 | $tokenSecret = $config['flickr']['token_secret'];
331 | if (!$tokenSecret) {
332 | return false;
333 | }
334 |
335 | $this->apiService = new ApiService($consumerKey, $consumerSecret, $token, $tokenSecret);
336 | $this->apiService->setLogger($this->logger);
337 |
338 | return true;
339 | }
340 |
341 | private function signalHandlerSetup()
342 | {
343 | if (!function_exists('pcntl_signal') || !function_exists('pcntl_signal_dispatch')) {
344 | throw new SignalException('pcntl_signal function not found. You need to install pcntl PHP extention.');
345 | }
346 |
347 | declare(ticks=1);
348 |
349 | pcntl_signal(SIGTERM, [$this, 'signalHandler']);
350 | /** @uses $this::signalHandler() */
351 | pcntl_signal(SIGINT, [$this, 'signalHandler']);
352 | /** @uses $this::signalHandler() */
353 | pcntl_signal(SIGHUP, [$this, 'signalHandler']);
354 | /** @uses $this::signalHandler() */
355 | }
356 |
357 | /**
358 | * @param int $signal
359 | */
360 | private function signalHandler(int $signal)
361 | {
362 | $this->exit++;
363 |
364 | if ($this->exit >= 2) {
365 | throw new SignalException(sprintf('Signal %d', $signal));
366 | }
367 | }
368 |
369 | /**
370 | * @param InputInterface $input
371 | * @param OutputInterface $output
372 | * @return int
373 | */
374 | protected function execute(InputInterface $input, OutputInterface $output): int
375 | {
376 | $this->setup($input, $output);
377 |
378 | return $this->getExit();
379 | }
380 | }
381 |
--------------------------------------------------------------------------------
/src/TheFox/FlickrCli/Command/PiwigoCommand.php:
--------------------------------------------------------------------------------
1 | connection;
33 | }
34 |
35 | /**
36 | * @param Connection $connection
37 | */
38 | public function setConnection(Connection $connection)
39 | {
40 | $this->connection = $connection;
41 | }
42 |
43 | protected function configure()
44 | {
45 | parent::configure();
46 |
47 | $this->setName('piwigo');
48 | $this->setDescription('Upload files from Piwigo to Flickr');
49 |
50 | $this->addOption('piwigo-uploads', null, InputOption::VALUE_REQUIRED, "Path to the Piwigo 'uploads' directory");
51 | }
52 |
53 | /**
54 | * Executes the current command.
55 | *
56 | * @param InputInterface $input An InputInterface instance
57 | * @param OutputInterface $output An OutputInterface instance
58 | *
59 | * @return int
60 | */
61 | protected function execute(InputInterface $input, OutputInterface $output): int
62 | {
63 | parent::execute($input, $output);
64 |
65 | $this->checkPiwigoConfig();
66 | $this->setupPiwigoConnection();
67 |
68 | $count = $this->getConnection()->query('SELECT COUNT(*) AS count FROM images')->fetchColumn();
69 | $output->writeln(sprintf('%s images found in the Piwigo database', number_format($count)));
70 |
71 | $piwigoUploadsPath = $input->getOption('piwigo-uploads');
72 |
73 | // Photos.
74 | $images = $this->getConnection()->query('SELECT * FROM images')->fetchAll();
75 | foreach ($images as $image) {
76 | $this->processOne($image, $piwigoUploadsPath);
77 | }
78 |
79 | return $this->getExit();
80 | }
81 |
82 | /**
83 | * @throws RuntimeException
84 | */
85 | private function checkPiwigoConfig()
86 | {
87 | $config = $this->getConfig();
88 |
89 | // Piwigo.
90 | if (!isset($config['piwigo'])
91 | || !isset($config['piwigo']['dbname'])
92 | || !isset($config['piwigo']['dbuser'])
93 | || !isset($config['piwigo']['dbpass'])
94 | || !isset($config['piwigo']['dbhost'])
95 | ) {
96 | throw new RuntimeException('Please set the all of the following options in the \'piwigo\' section of config.yml: dbname, dbuser, dbpass, & dbhost.');
97 | }
98 | }
99 |
100 | private function setupPiwigoConnection()
101 | {
102 | $config = $this->getConfig();
103 |
104 | $dbConfig = new Configuration();
105 | $connectionParams = [
106 | 'dbname' => $config['piwigo']['dbname'],
107 | 'user' => $config['piwigo']['dbuser'],
108 | 'password' => $config['piwigo']['dbpass'],
109 | 'host' => $config['piwigo']['dbhost'],
110 | 'driver' => 'pdo_mysql',
111 | ];
112 | $conn = DriverManager::getConnection($connectionParams, $dbConfig);
113 |
114 | $this->setConnection($conn);
115 | }
116 |
117 | /**
118 | * @param $image
119 | * @param $piwigoUploadsPath
120 | * @throws RuntimeException
121 | */
122 | protected function processOne($image, $piwigoUploadsPath)
123 | {
124 | // Check file.
125 | $filePath = $piwigoUploadsPath . substr($image['path'], 9);
126 | if (!file_exists($filePath)) {
127 | throw new RuntimeException(sprintf('File not found: %s', $filePath));
128 | }
129 |
130 | // Figure out the privacy level.
131 | // 1 = Contacts
132 | // 2 = Friends
133 | // 4 = Family
134 | // 8 = Admins
135 | $isPublic = false;
136 | $isFriend = false;
137 | $isFamily = false;
138 | switch ($image['level']) {
139 | case 0:
140 | $isPublic = true;
141 | break;
142 | case 1:
143 | case 2:
144 | case 4:
145 | $isFriend = true;
146 | $isFamily = true;
147 | break;
148 | case 8:
149 | default:
150 | break;
151 | }
152 |
153 | // Get tags (including a checksum machine tag).
154 | $cats = $this->getConnection()->prepare('SELECT t.name FROM image_tag it JOIN tags t ON it.tag_id=t.id WHERE it.image_id=:id');
155 | $cats->bindValue('id', $image['id']);
156 | $cats->execute();
157 |
158 | if (empty($image['md5sum'])) {
159 | $md5sum = md5_file($filePath);
160 | } else {
161 | $md5sum = $image['md5sum'];
162 | }
163 | $tags = [sprintf('checksum:md5=%s', $md5sum)];
164 | while ($cat = $cats->fetch()) {
165 | $tags[] = $cat['name'];
166 | }
167 |
168 | // Make sure it's not already on Flickr (by MD5 checksum only).
169 | $apiFactory = $this->getApiService()->getApiFactory();
170 | $md5search = $apiFactory->call('flickr.photos.search', [
171 | 'user_id' => 'me',
172 | 'tags' => sprintf('checksum:md5=%s', $md5sum),
173 | ]);
174 | if (((int)$md5search->photos['total']) > 0) {
175 | $this->getOutput()->writeln(sprintf('Already exists: %s', $image['name']));
176 | return;
177 | }
178 |
179 | // Upload to Flickr.
180 | $this->getOutput()->write(sprintf('Uploading: %s', $image['name']));
181 | $comment = $image['comment'];
182 | $xml = $apiFactory->upload($filePath, $image['name'], $comment, $tags, $isPublic, $isFriend, $isFamily);
183 | $photoId = isset($xml->photoid) ? (int)$xml->photoid : 0;
184 | $stat = isset($xml->attributes()->stat) ? strtolower((string)$xml->attributes()->stat) : '';
185 | $successful = $stat == 'ok' && $photoId != 0;
186 | if (!$successful) {
187 | throw new RuntimeException(sprintf('Failed to upload %s to %s', $filePath, $image['name']));
188 | }
189 |
190 | // Add to albums (categories, in Piwigo parlance).
191 | $this->getOutput()->write(' [photosets]');
192 | $sql = 'SELECT c.name FROM image_category ic JOIN categories c ON ic.category_id=c.id WHERE ic.image_id=:id';
193 | $cats = $this->getConnection()->prepare($sql);
194 | $cats->bindValue('id', $image['id']);
195 | $cats->execute();
196 | while ($cat = $cats->fetch()) {
197 | $photosetId = $this->getPhotosetId($cat['name'], $photoId);
198 | $apiFactory->call('flickr.photosets.addPhoto', [
199 | 'photoset_id' => $photosetId,
200 | 'photo_id' => $photoId,
201 | ]);
202 | }
203 |
204 | // Add to an import photoset.
205 | $importFromPiwigoId = $this->getPhotosetId('Imported from Piwigo', $photoId);
206 | $apiFactory->call('flickr.photosets.addPhoto', [
207 | 'photoset_id' => $importFromPiwigoId,
208 | 'photo_id' => $photoId,
209 | ]);
210 |
211 | // Set location on Flickr.
212 | if (!empty($image['latitude']) && !empty($image['longitude'])) {
213 | $this->getOutput()->write(' [location]');
214 | $apiFactory->call('flickr.photos.geo.setLocation', [
215 | 'photo_id' => $photoId,
216 | 'lat' => $image['latitude'],
217 | 'lon' => $image['longitude'],
218 | ]);
219 | } else {
220 | $this->getOutput()->write(' [no location]');
221 | }
222 |
223 | $this->getOutput()->writeln(' -- done');
224 | }
225 |
226 | /**
227 | * Get a photoset's ID from a name, creating a new photo set if required.
228 | * Case insensitive.
229 | *
230 | * @param string $photosetName
231 | * @param int $primaryPhotoId
232 | * @return int
233 | */
234 | protected function getPhotosetId($photosetName, $primaryPhotoId)
235 | {
236 | $apiFactory = $this->getApiService()->getApiFactory();
237 |
238 | // First get all existing albums (once only).
239 | if (!is_array($this->photosets)) {
240 | $this->photosets = [];
241 | $getList = $apiFactory->call('flickr.photosets.getList');
242 | /**
243 | * @var string $n
244 | * @var \SimpleXMLElement $photoset
245 | */
246 | foreach ($getList->photosets->photoset as $n => $photoset) {
247 | $this->photosets[(int)$photoset->attributes()->id] = (string)$photoset->title;
248 | }
249 | }
250 |
251 | // See if we've already got it.
252 | foreach ($this->photosets as $id => $name) {
253 | if (mb_strtolower($photosetName) != mb_strtolower($name)) {
254 | continue;
255 | }
256 |
257 | return (int)$id;
258 | }
259 |
260 | // Otherwise, create it.
261 | $this->getOutput()->write(sprintf(' [creating new photoset: %s]', $photosetName));
262 | $newPhotoset = $apiFactory->call('flickr.photosets.create', [
263 | 'title' => $photosetName,
264 | 'primary_photo_id' => $primaryPhotoId,
265 | ]);
266 | $newId = (int)$newPhotoset->photoset->attributes()->id;
267 | $this->photosets[$newId] = $photosetName;
268 | return $newId;
269 | }
270 | }
271 |
--------------------------------------------------------------------------------
/src/TheFox/FlickrCli/Command/UploadCommand.php:
--------------------------------------------------------------------------------
1 | setName('upload');
27 | $this->setDescription('Upload files to Flickr.');
28 |
29 | $this->addOption('description', 'd', InputOption::VALUE_OPTIONAL, 'Description for all uploaded files.');
30 |
31 | $csvTagsDesc = 'Comma separated names. For example: --tags=tag1,"Tag two"';
32 | $this->addOption('tags', 't', InputOption::VALUE_OPTIONAL, $csvTagsDesc);
33 |
34 | $csvSetsDesc = 'Comma separated names. For example: --sets="Set one",set2';
35 | $this->addOption('sets', 's', InputOption::VALUE_OPTIONAL, $csvSetsDesc);
36 |
37 | $this->addOption('recursive', 'r', InputOption::VALUE_NONE, 'Recurse into directories.');
38 | $this->addOption('dry-run', null, InputOption::VALUE_NONE, 'Show what would have been transferred.');
39 | $this->addOption('move', 'm', InputOption::VALUE_OPTIONAL, 'Move uploaded files to this directory.');
40 |
41 | $this->addArgument('directory', InputArgument::IS_ARRAY, 'Path to directories.');
42 | }
43 |
44 | /**
45 | * @param InputInterface $input
46 | * @param OutputInterface $output
47 | * @return int
48 | */
49 | protected function execute(InputInterface $input, OutputInterface $output): int
50 | {
51 | parent::execute($input, $output);
52 |
53 | $apiService = $this->getApiService();
54 |
55 | if ($input->hasOption('description') && $input->getOption('description')) {
56 | $description = $input->getOption('description');
57 | $this->getLogger()->info(sprintf('Description: %s', $description));
58 | } else {
59 | $description = null;
60 | }
61 |
62 | if ($input->hasOption('tags') && $input->getOption('tags')) {
63 | $tags = $input->getOption('tags');
64 | $this->getLogger()->debug(sprintf('Tags String: %s', $tags));
65 | } else {
66 | $tags = null;
67 | }
68 |
69 | $recursive = $input->getOption('recursive');
70 | $dryrun = $input->getOption('dry-run');
71 |
72 | //$metadata = new Metadata($config['flickr']['consumer_key'], $config['flickr']['consumer_secret']);
73 | //$metadata->setOauthAccess($config['flickr']['token'], $config['flickr']['token_secret']);
74 |
75 | //$guzzleAdapter = new RezzzaGuzzleAdapter();
76 | $guzzleAdapterVerbose = new RezzzaGuzzleAdapter();
77 | $guzzleAdapterClient = $guzzleAdapterVerbose->getClient();
78 | $guzzleAdapterClientConfig = $guzzleAdapterClient->getConfig();
79 |
80 | $curlOptions = $guzzleAdapterClientConfig->get(GuzzleHttpClient::CURL_OPTIONS);
81 | $curlOptions[CURLOPT_CONNECTTIMEOUT] = 60;
82 | $curlOptions[CURLOPT_NOPROGRESS] = false;
83 |
84 | $timePrev = 0;
85 | $uploadedTotal = 0;
86 | $uploadedPrev = 0;
87 | $uploadedDiffPrev = [0, 0, 0, 0, 0];
88 |
89 | $curlOptions[CURLOPT_PROGRESSFUNCTION] = function ($ch, $dlTotal = 0, $dlNow = 0, $ulTotal = 0, $ulNow = 0) use ($timePrev, $uploadedTotal, $uploadedPrev, $uploadedDiffPrev) {
90 |
91 | $uploadedDiff = $ulNow - $uploadedPrev;
92 | $uploadedPrev = $ulNow;
93 | $uploadedTotal += $uploadedDiff;
94 |
95 | $percent = 0;
96 | if ($ulTotal) {
97 | $percent = $ulNow / $ulTotal * 100;
98 | }
99 | if ($percent > 100) {
100 | $percent = 100;
101 | }
102 |
103 | $progressbarUploaded = round($percent / 100 * FlickrCli::UPLOAD_PROGRESSBAR_ITEMS);
104 | $progressbarRest = FlickrCli::UPLOAD_PROGRESSBAR_ITEMS - $progressbarUploaded;
105 |
106 | $uploadedDiffStr = '';
107 | $timeCur = time();
108 | if ($timeCur != $timePrev) {
109 | //$timePrev = $timeCur;
110 |
111 | $uploadedDiff = ($uploadedDiff + array_sum($uploadedDiffPrev)) / 6;
112 | array_shift($uploadedDiffPrev);
113 | $uploadedDiffPrev[] = $uploadedDiff;
114 |
115 | if ($uploadedDiff > 0) {
116 | $bytesize = new ByteSize();
117 | $uploadedDiffStr = $bytesize->format($uploadedDiff) . '/s';
118 | }
119 | }
120 |
121 | printf(
122 | "[file] %6.2f%% [%s%s] %s %10s\x1b[0K\r",
123 | $percent,
124 | str_repeat('#', $progressbarUploaded),
125 | str_repeat(' ', $progressbarRest),
126 | number_format($ulNow),
127 | $uploadedDiffStr
128 | );
129 |
130 | pcntl_signal_dispatch();
131 |
132 | return $this->getExit() >= 2 ? 1 : 0;
133 | };
134 | $guzzleAdapterClientConfig->set(GuzzleHttpClient::CURL_OPTIONS, $curlOptions);
135 |
136 | //$apiFactory = new ApiFactory($metadata, $guzzleAdapter);
137 | $apiFactory = $apiService->getApiFactory();
138 | $metadata = $apiFactory->getMetadata();
139 | $apiFactoryVerbose = new ApiFactory($metadata, $guzzleAdapterVerbose);
140 |
141 | if ($input->getOption('sets')) {
142 | $photosetNames = preg_split('/,/', $input->getOption('sets'));
143 | } else {
144 | $photosetNames = [];
145 | }
146 |
147 | $photosetAll = [];
148 | $photosetAllLower = [];
149 |
150 | $apiFactory = $apiService->getApiFactory();
151 | $xml = $apiFactory->call('flickr.photosets.getList');
152 |
153 | /**
154 | * @var int $n
155 | * @var SimpleXMLElement $photoset
156 | */
157 | foreach ($xml->photosets->photoset as $n => $photoset) {
158 | pcntl_signal_dispatch();
159 | if ($this->getExit()) {
160 | break;
161 | }
162 |
163 | $id = (int)$photoset->attributes()->id;
164 | $title = (string)$photoset->title;
165 |
166 | $photosetAll[$id] = $title;
167 | $photosetAllLower[$id] = strtolower($title);
168 | }
169 |
170 | $photosets = [];
171 | $photosetsNew = [];
172 | foreach ($photosetNames as $photosetTitle) {
173 | $id = 0;
174 |
175 | foreach ($photosetAllLower as $photosetAllId => $photosetAllTitle) {
176 | if (strtolower($photosetTitle) != $photosetAllTitle) {
177 | continue;
178 | }
179 |
180 | $id = $photosetAllId;
181 | break;
182 | }
183 | if ($id) {
184 | $photosets[] = $id;
185 | } else {
186 | $photosetsNew[] = $photosetTitle;
187 | }
188 | }
189 |
190 | // Move files after they've been successfully uploaded?
191 | $configUploadedBaseDir = false;
192 | $move = $input->getOption('move');
193 | if (null !== $move) {
194 | $configUploadedBaseDir = dirname($move);
195 |
196 | $filesystem = new Filesystem();
197 | // Make the local directory if it doesn't exist.
198 | if (!$filesystem->exists($configUploadedBaseDir)) {
199 | $filesystem->mkdir($configUploadedBaseDir, 0755);
200 | $this->getLogger()->info(sprintf('Created directory: %s', $configUploadedBaseDir));
201 | }
202 | $this->getLogger()->info(sprintf('Uploaded files will be moved to: %s', $configUploadedBaseDir));
203 | }
204 |
205 | $totalFiles = 0;
206 | $totalFilesUploaded = 0;
207 | $fileErrors = 0;
208 | $filesFailed = [];
209 |
210 | $filter = function (SplFileInfo $file) {
211 | if (in_array($file->getFilename(), FlickrCli::FILES_INORE)) {
212 | return false;
213 | }
214 | return true;
215 | };
216 | $finder = new Finder();
217 | $finder->files()->filter($filter);
218 | if (!$recursive) {
219 | $finder->depth(0);
220 | }
221 |
222 | $bytesize = new ByteSize();
223 |
224 | $directories = $input->getArgument('directory');
225 | foreach ($directories as $argDir) {
226 | if ($configUploadedBaseDir) {
227 | $argDirReplaced = str_replace('/', '_', $argDir);
228 | $uploadBaseDirPath = sprintf('%s/%s', $configUploadedBaseDir, $argDirReplaced);
229 | } else {
230 | $uploadBaseDirPath = '';
231 | }
232 |
233 | $this->getLogger()->info(sprintf('[dir] upload dir: %s %s', $argDir, $uploadBaseDirPath));
234 |
235 | /** @var \Symfony\Component\Finder\SplFileInfo[] $files */
236 | $files = iterator_to_array($finder->in($argDir));
237 | sort($files);
238 | foreach ($files as $file) {
239 | pcntl_signal_dispatch();
240 | if ($this->getExit()) {
241 | break;
242 | }
243 |
244 | $fileName = $file->getFilename();
245 | $fileExt = $file->getExtension();
246 | $filePath = $file->getRealPath();
247 | $fileRelativePath = new SplFileInfo($file->getRelativePathname());
248 | $fileRelativePathStr = (string)$fileRelativePath;
249 | $dirRelativePath = $fileRelativePath->getPath();
250 |
251 | $uploadFileSize = filesize($filePath);
252 | //$uploadFileSizeLen = strlen(number_format($uploadFileSize));
253 | $uploadFileSizeFormatted = $bytesize->format($uploadFileSize);
254 |
255 | $uploadDirPath = '';
256 | if ($uploadBaseDirPath) {
257 | $uploadDirPath = sprintf('%s/%s', $uploadBaseDirPath, $dirRelativePath);
258 |
259 | $filesystem = new Filesystem();
260 | if (!$filesystem->exists($uploadDirPath)) {
261 | $this->getLogger()->info(sprintf('[dir] create "%s"', $uploadDirPath));
262 | $filesystem->mkdir($uploadDirPath);
263 | }
264 | }
265 |
266 | $totalFiles++;
267 |
268 | if (!in_array(strtolower($fileExt), FlickrCli::ACCEPTED_EXTENTIONS)) {
269 | $fileErrors++;
270 | $filesFailed[] = $fileRelativePathStr;
271 | $this->getLogger()->warning(sprintf('[file] invalid extension: %s', $fileRelativePathStr));
272 |
273 | continue;
274 | }
275 |
276 | if ($dryrun) {
277 | $this->getLogger()->info(sprintf(
278 | "[file] dry upload '%s' '%s' %s",
279 | $fileRelativePathStr,
280 | $dirRelativePath,
281 | $uploadFileSizeFormatted
282 | ))
283 | ;
284 | continue;
285 | }
286 |
287 | $this->getLogger()->info(sprintf('[file] upload "%s" %s', $fileRelativePathStr, $uploadFileSizeFormatted));
288 | try {
289 | $xml = $apiFactoryVerbose->upload($filePath, $fileName, $description, $tags);
290 |
291 | print "\n";
292 | } catch (Exception $e) {
293 | $this->getLogger()->error(sprintf('[file] upload: %s', $e->getMessage()));
294 | $xml = null;
295 | }
296 |
297 | if ($xml) {
298 | $photoId = isset($xml->photoid) ? (int)$xml->photoid : 0;
299 | $stat = isset($xml->attributes()->stat) ? strtolower((string)$xml->attributes()->stat) : '';
300 | $successful = $stat == 'ok' && $photoId != 0;
301 | } else {
302 | $photoId = 0;
303 | $successful = false;
304 | }
305 |
306 | if ($successful) {
307 | $logLine = 'OK';
308 | $totalFilesUploaded++;
309 |
310 | if ($uploadDirPath) {
311 | $this->getLogger()->info(sprintf('[file] move to uploaded dir: %s', $uploadDirPath));
312 |
313 | $filesystem = new Filesystem();
314 | $filesystem->rename($filePath, sprintf('%s/%s', $uploadDirPath, $fileName));
315 | }
316 | } else {
317 | $logLine = 'FAILED';
318 | $fileErrors++;
319 | $filesFailed[] = $fileRelativePathStr;
320 | }
321 | $this->getLogger()->info(sprintf('[file] status: %s - ID %s', $logLine, $photoId));
322 |
323 | if (!$successful) {
324 | continue;
325 | }
326 |
327 | if ($photosetsNew) {
328 | foreach ($photosetsNew as $photosetTitle) {
329 | $this->getLogger()->info(sprintf('[photoset] create %s ... ', $photosetTitle));
330 |
331 | $xml = null;
332 | try {
333 | $xml = $apiFactory->call('flickr.photosets.create', [
334 | 'title' => $photosetTitle,
335 | 'primary_photo_id' => $photoId,
336 | ]);
337 | } catch (Exception $e) {
338 | $this->getLogger()->critical(sprintf('[photoset] create %s FAILED: %s', $photosetTitle, $e->getMessage()));
339 | return 1;
340 | }
341 |
342 | if ((string)$xml->attributes()->stat == 'ok') {
343 | $photosetId = (int)$xml->photoset->attributes()->id;
344 | $photosets[] = $photosetId;
345 |
346 | $this->getLogger()->info(sprintf('[photoset] create %s OK - ID %s', $photosetTitle, $photosetId));
347 | } else {
348 | $code = (int)$xml->err->attributes()->code;
349 | $this->getLogger()->critical(sprintf('[photoset] create %s FAILED: %s', $photosetTitle, $code));
350 | return 1;
351 | }
352 | }
353 | $photosetsNew = null;
354 | }
355 |
356 | if (count($photosets)) {
357 | $this->getLogger()->info('[file] add to sets ... ');
358 |
359 | $logLine = [];
360 | foreach ($photosets as $photosetId) {
361 | $logLine[] = substr($photosetId, -5);
362 |
363 | try {
364 | $xml = $apiFactory->call('flickr.photosets.addPhoto', [
365 | 'photoset_id' => $photosetId,
366 | 'photo_id' => $photoId,
367 | ]);
368 | } catch (Exception $e) {
369 | $this->getLogger()->critical(sprintf('[file] add to sets FAILED: %s', $e->getMessage()));
370 | return 1;
371 | }
372 |
373 | if ($xml->attributes()->stat == 'ok') {
374 | $logLine[] = 'OK';
375 | } else {
376 | if (isset($xml->err)) {
377 | $code = (int)$xml->err->attributes()->code;
378 | if ($code == 3) {
379 | $logLine[] = 'OK';
380 | } else {
381 | $this->getLogger()->critical(sprintf('[file] add to sets FAILED: %d', $code));
382 | return 1;
383 | }
384 | } else {
385 | $this->getLogger()->critical('[file] add to sets FAILED');
386 | return 1;
387 | }
388 | }
389 | }
390 |
391 | $this->getLogger()->info(sprintf('[file] added to sets: %s', join(' ', $logLine)));
392 | }
393 | }
394 | }
395 |
396 | if ($uploadedTotal > 0) {
397 | $uploadedTotalStr = $bytesize->format($uploadedTotal);
398 | } else {
399 | $uploadedTotalStr = 0;
400 | }
401 |
402 | $this->getLogger()->notice(sprintf('[main] total uploaded: %s', $uploadedTotalStr));
403 | $this->getLogger()->notice(sprintf('[main] total files: %d', $totalFiles));
404 | $this->getLogger()->notice(sprintf('[main] files uploaded: %d', $totalFilesUploaded));
405 |
406 | $filesFailedMsg = count($filesFailed) ? "\n" . join("\n", $filesFailed) : '';
407 | $this->getLogger()->notice(sprintf('[main] files failed: %s%s', $fileErrors, $filesFailedMsg));
408 |
409 | return $this->getExit();
410 | }
411 | }
412 |
--------------------------------------------------------------------------------
/src/TheFox/FlickrCli/Exception/SignalException.php:
--------------------------------------------------------------------------------
1 | logger;
17 | }
18 |
19 | /**
20 | * @param LoggerInterface $logger
21 | */
22 | public function setLogger(LoggerInterface $logger)
23 | {
24 | $this->logger = $logger;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/TheFox/FlickrCli/Service/ApiService.php:
--------------------------------------------------------------------------------
1 | setLogger(new NullLogger());
44 |
45 | $this->consumerKey = $consumerKey;
46 | $this->consumerSecret = $consumerSecret;
47 | $this->token = $token;
48 | $this->tokenSecret = $tokenSecret;
49 | }
50 |
51 | /**
52 | * @return ApiFactory
53 | */
54 | public function getApiFactory(): ApiFactory
55 | {
56 | // Set up the Flickr API.
57 | $metadata = new Metadata($this->consumerKey, $this->consumerSecret);
58 | $metadata->setOauthAccess($this->token, $this->tokenSecret);
59 | $adapter = new RezzzaGuzzleAdapter();
60 | $apiFactory = new ApiFactory($metadata, $adapter);
61 |
62 | return $apiFactory;
63 | }
64 |
65 | /**
66 | * @return array
67 | */
68 | public function getPhotosetTitles(): array
69 | {
70 | $apiFactory = $this->getApiFactory();
71 |
72 | $this->getLogger()->info('[main] get photosets');
73 | $xml = $apiFactory->call('flickr.photosets.getList');
74 |
75 | /**
76 | * @var int $n
77 | * @var SimpleXMLElement $photoset
78 | */
79 | foreach ($xml->photosets->photoset as $n => $photoset) {
80 | $id = (int)$photoset->attributes()->id;
81 | $photosetsTitles[$id] = (string)$photoset->title;
82 | }
83 |
84 | asort($photosetsTitles);
85 |
86 | return $photosetsTitles;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/TheFox/OAuth/Common/Http/Client/GuzzleStreamClient.php:
--------------------------------------------------------------------------------
1 | 'close'];
32 | $headers = array_merge($headers, $extraHeaders);
33 | $response = null;
34 |
35 | if ($method == 'POST') {
36 | $request = $client->post($endpoint->getAbsoluteUri(), $headers, $requestBody);
37 | $response = $request->send();
38 | } elseif ($method == 'GET') {
39 | throw new InvalidArgumentException('"GET" request not implemented.');
40 | }
41 |
42 | if ($response && !$response->isSuccessful()) {
43 | throw new TokenResponseException('Failed to request token.');
44 | }
45 |
46 | $responseHtml = (string)$response->getBody();
47 | return $responseHtml;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/TheFox/OAuth/OAuth1/Service/Flickr.php:
--------------------------------------------------------------------------------
1 | baseApiUri = new Uri('https://api.flickr.com/services/rest/?');
37 | }
38 | }
39 |
40 | /**
41 | * @return Uri
42 | */
43 | public function getRequestTokenEndpoint(): Uri
44 | {
45 | return new Uri('https://www.flickr.com/services/oauth/request_token');
46 | }
47 |
48 | /**
49 | * @return Uri
50 | */
51 | public function getAuthorizationEndpoint(): Uri
52 | {
53 | return new Uri('https://www.flickr.com/services/oauth/authorize');
54 | }
55 |
56 | /**
57 | * @return Uri
58 | */
59 | public function getAccessTokenEndpoint(): Uri
60 | {
61 | return new Uri('https://www.flickr.com/services/oauth/access_token');
62 | }
63 |
64 | /**
65 | * @param string $responseBody
66 | * @return StdOAuth1Token
67 | * @throws TokenResponseException
68 | */
69 | protected function parseRequestTokenResponse($responseBody): StdOAuth1Token
70 | {
71 | parse_str($responseBody, $data);
72 | if (null === $data || !is_array($data)) {
73 | throw new TokenResponseException('Unable to parse response.');
74 | } elseif (!isset($data['oauth_callback_confirmed']) || $data['oauth_callback_confirmed'] != 'true') {
75 | throw new TokenResponseException('Error in retrieving token.');
76 | }
77 | return $this->parseAccessTokenResponse($responseBody);
78 | }
79 |
80 | /**
81 | * @param string $responseBody
82 | * @return StdOAuth1Token
83 | * @throws TokenResponseException
84 | */
85 | protected function parseAccessTokenResponse($responseBody): StdOAuth1Token
86 | {
87 | #print "parseAccessTokenResponse\n";
88 |
89 | parse_str($responseBody, $data);
90 | if ($data === null || !is_array($data)) {
91 | throw new TokenResponseException('Unable to parse response.');
92 | } elseif (isset($data['error'])) {
93 | throw new TokenResponseException('Error in retrieving token: "' . $data['error'] . '"');
94 | }
95 |
96 | $token = new StdOAuth1Token();
97 | $token->setRequestToken($data['oauth_token']);
98 | $token->setRequestTokenSecret($data['oauth_token_secret']);
99 | $token->setAccessToken($data['oauth_token']);
100 | $token->setAccessTokenSecret($data['oauth_token_secret']);
101 | $token->setEndOfLife(StdOAuth1Token::EOL_NEVER_EXPIRES);
102 | unset($data['oauth_token'], $data['oauth_token_secret']);
103 | $token->setExtraParams($data);
104 |
105 | return $token;
106 | }
107 | }
108 |
--------------------------------------------------------------------------------