1+ #!/usr/bin/env php
2+ <?php
3+
4+ // API Credentials
5+ // You can either provide them as environment variables
6+ // or hard-code them in the empty strings below.
7+ $ baseUrl = getenv ('BS_URL ' ) ?: '' ;
8+ $ clientId = getenv ('BS_TOKEN_ID ' ) ?: '' ;
9+ $ clientSecret = getenv ('BS_TOKEN_SECRET ' ) ?: '' ;
10+
11+ // Script logic
12+ ////////////////
13+
14+ // Define the time we wait in between making API requests,
15+ // to help keep within rate limits and avoid exhausting resources.
16+ $ apiPauseMicrosecs = 100 ;
17+
18+ // Clean up the base path
19+ $ baseUrl = rtrim ($ baseUrl , '/ ' );
20+
21+ // Get all items from the system keyed by ID
22+ $ shelvesById = keyById (getAllOfAtListEndpoint ("api/shelves " , []));
23+ $ booksById = keyById (getAllOfAtListEndpoint ("api/books " , []));
24+
25+ // Fetch books that are on each shelf
26+ foreach ($ shelvesById as $ id => $ shelf ) {
27+ $ shelvesById [$ id ]['books ' ] = getBooksForShelf ($ id );
28+ usleep ($ apiPauseMicrosecs );
29+ }
30+
31+ // For each book, fetch its contents list
32+ foreach ($ booksById as $ id => $ book ) {
33+ $ booksById [$ id ]['contents ' ] = apiGetJson ("api/books/ {$ id }" )['contents ' ] ?? [];
34+ usleep ($ apiPauseMicrosecs );
35+ }
36+
37+ // Cycle through the shelves and display their contents
38+ $ isBookShownById = [];
39+ foreach ($ shelvesById as $ id => $ shelf ) {
40+ output ($ shelf , 'bookshelf ' , [false ]);
41+ $ bookCount = count ($ shelf ['books ' ]);
42+ for ($ i =0 ; $ i < $ bookCount ; $ i ++) {
43+ $ bookId = $ shelf ['books ' ][$ i ];
44+ $ book = $ booksById [$ bookId ] ?? null ;
45+ if ($ book ) {
46+ outputBookAndContents ($ book , [false , $ i === $ bookCount - 1 ]);
47+ $ isBookShownById [strval ($ book ['id ' ])] = true ;
48+ }
49+ }
50+ }
51+
52+ // Cycle through books and display any that have not been
53+ // part of a shelve's output
54+ foreach ($ booksById as $ id => $ book ) {
55+ if (isset ($ isBookShownById [$ id ])) {
56+ continue ;
57+ }
58+
59+ outputBookAndContents ($ book , [false ]);
60+ }
61+
62+ /**
63+ * Output a book for display, along with its contents.
64+ */
65+ function outputBookAndContents (array $ book , array $ depthPath ): void
66+ {
67+ output ($ book , 'book ' , $ depthPath );
68+ $ childCount = count ($ book ['contents ' ]);
69+ for ($ i =0 ; $ i < $ childCount ; $ i ++) {
70+ $ child = $ book ['contents ' ][$ i ];
71+ $ childPath = array_merge ($ depthPath , [($ i === $ childCount - 1 )]);
72+ output ($ child , $ child ['type ' ], $ childPath );
73+ $ pages = $ child ['pages ' ] ?? [];
74+ $ pageCount = count ($ pages );
75+ for ($ j =0 ; $ j < count ($ pages ); $ j ++) {
76+ $ page = $ pages [$ j ];
77+ $ innerPath = array_merge ($ childPath , [($ j === $ pageCount - 1 )]);
78+ output ($ page , 'page ' , $ innerPath );
79+ }
80+ }
81+ }
82+
83+ /**
84+ * Output a single item for display.
85+ */
86+ function output (array $ item , string $ type , array $ depthPath ): void
87+ {
88+ $ upperType = strtoupper ($ type );
89+ $ prefix = '' ;
90+ $ depth = count ($ depthPath );
91+ for ($ i =0 ; $ i < $ depth ; $ i ++) {
92+ $ isLastAtDepth = $ depthPath [$ i ];
93+ $ end = ($ i === $ depth - 1 );
94+ if ($ end ) {
95+ $ prefix .= $ isLastAtDepth ? '└ ' : '├ ' ;
96+ } else {
97+ $ prefix .= $ isLastAtDepth ? ' ' : '│ ' ;
98+ }
99+ }
100+ echo $ prefix . "── {$ upperType } {$ item ['id ' ]}: {$ item ['name ' ]}\n" ;
101+ }
102+
103+ /**
104+ * Key an array of array-based data objects by 'id' value.
105+ */
106+ function keyById (array $ data ): array
107+ {
108+ $ byId = [];
109+ foreach ($ data as $ item ) {
110+ $ id = $ item ['id ' ];
111+ $ byId [$ id ] = $ item ;
112+ }
113+ return $ byId ;
114+ }
115+
116+ /**
117+ * Get the books for the given shelf ID.
118+ * Returns an array of the book IDs.
119+ */
120+ function getBooksForShelf (int $ shelfId ): array
121+ {
122+ $ resp = apiGetJson ("api/shelves/ {$ shelfId }" );
123+ return array_map (function ($ bookData ) {
124+ return $ bookData ['id ' ];
125+ }, $ resp ['books ' ] ?? []);
126+ }
127+
128+ /**
129+ * Consume all items from the given API listing endpoint.
130+ */
131+ function getAllOfAtListEndpoint (string $ endpoint , array $ params ): array
132+ {
133+ global $ apiPauseMicrosecs ;
134+ $ count = 100 ;
135+ $ offset = 0 ;
136+ $ all = [];
137+
138+ do {
139+ $ endpoint = $ endpoint . '? ' . http_build_query (array_merge ($ params , ['count ' => $ count , 'offset ' => $ offset ]));
140+ $ resp = apiGetJson ($ endpoint );
141+
142+ $ total = $ resp ['total ' ] ?? 0 ;
143+ $ new = $ resp ['data ' ] ?? [];
144+ array_push ($ all , ...$ new );
145+ $ offset += $ count ;
146+ usleep ($ apiPauseMicrosecs );
147+ } while ($ offset < $ total );
148+
149+ return $ all ;
150+ }
151+
152+ /**
153+ * Make a simple GET HTTP request to the API.
154+ */
155+ function apiGet (string $ endpoint ): string
156+ {
157+ global $ baseUrl , $ clientId , $ clientSecret ;
158+ $ url = rtrim ($ baseUrl , '/ ' ) . '/ ' . ltrim ($ endpoint , '/ ' );
159+ $ opts = ['http ' => ['header ' => "Authorization: Token {$ clientId }: {$ clientSecret }" ]];
160+ $ context = stream_context_create ($ opts );
161+ return @file_get_contents ($ url , false , $ context );
162+ }
163+
164+ /**
165+ * Make a simple GET HTTP request to the API &
166+ * decode the JSON response to an array.
167+ */
168+ function apiGetJson (string $ endpoint ): array
169+ {
170+ $ data = apiGet ($ endpoint );
171+ $ array = json_decode ($ data , true );
172+
173+ if (!is_array ($ array )) {
174+ dd ("Failed request to {$ endpoint }" , $ data );
175+ }
176+
177+ return $ array ;
178+ }
179+
180+ /**
181+ * DEBUG: Dump out the given variables and exit.
182+ */
183+ function dd (...$ args )
184+ {
185+ foreach ($ args as $ arg ) {
186+ var_dump ($ arg );
187+ }
188+ exit (1 );
189+ }
0 commit comments