Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
jonas-dp committed Dec 4, 2019
0 parents commit 553d6e2
Show file tree
Hide file tree
Showing 17 changed files with 711 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.vscode/
libsearch.html
cache/
__pycache__/
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2019 Jonas De Pelsmaeker

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# LibSearch

LibSearch = Goodreads + bibliotheek.be. Get availabilities of your Goodreads to-read list from Flanders' public libraries.

## Getting Started

Download the repo and add your configuration in config.json. As an example is given in config.json.

* Branch name is the municipality you want to search (enter 'Wetteren' for 'wetteren.bibliotheek.be')
* libraries are the name of the individual libraries in the municipality (this can be empty to search all the libraries in the municipality)

### Prerequisites

Add API keys to config.json:

* [Goodreads developer key & secret](https://www.goodreads.com/api/keys)
* [CultuurConnect API key](https://www.cultuurconnect.be/api) or use test key (b8157a8a17c57162b1c9d8096c5f3620)

You'll need Python (3.6.5 minimum) and some libraries:

```
pip install rauth
pip install aiohttp
```

## Running

Run libsearch.py.
If it's the first time the program is running, it will open the authorisation page for Goodreads.
When you've accepted the authorisation, enter 'y' in the console.

Your webbrowser will open, displaying the results.

## License

This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details

## Acknowledgments

* CultuurConnect
21 changes: 21 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"Cultuurconnect": {
"authKey": "<key>",
"branches": [
{
"name": "Wetteren",
"libraries": [
"Hoofdbibliotheek",
"Massemen"
]
},
{
"name": "Lokeren"
}
]
},
"Goodreads": {
"devKey": "<key>",
"devSecret": "<secret>"
}
}
33 changes: 33 additions & 0 deletions libsearch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import asyncio

from src.Clients.Cultuurconnect import Cultuurconnect
from src.Clients.Goodreads import Goodreads
from src.HTMLOutput import HTMLOutput
from src.Configuration import Configuration
from src.Cache import Cache
from src.Catalogue import Catalogue

def main():

Configuration().load()
books = Goodreads().get_books()

catalogue = Catalogue()
catalogue.set_books(books)

cultuurconnect = Cultuurconnect()
loop = asyncio.get_event_loop()
catalogue.books = loop.run_until_complete(cultuurconnect.search_books(catalogue.books))

Cache().save_catalogue(catalogue)

books = loop.run_until_complete(cultuurconnect.get_availibities_of_books(catalogue.books))
loop.close()

HTMLOutput().createHTML(catalogue)
HTMLOutput().openHTML()


if __name__ == "__main__":
main()

21 changes: 21 additions & 0 deletions src/Availability.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class Availability(object):

is_available = None
branch = None
library = None
status = None
subloc = None
zizo_image_url = None
shelfmark = None
publication = None
return_date = None

def __init__(self, is_available: bool, branch: str, library: str, status: str, subloc: str, shelfmark: str, publication: str):
self.is_available = is_available
self.branch = branch
self.library = library
self.status = status
self.subloc = subloc
self.shelfmark = shelfmark
self.publication = publication

143 changes: 143 additions & 0 deletions src/Book.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
from datetime import date

class Book(object):

frabl = None
vlacc = None
isbn = None
pages = None
availabilities = None
detail_page = None
goodreads_page = None
goodreads_cover = None
is_available = False

def __init__(self, author: str, title: str, goodreads_id: str):
self.author = author
self.title = title
self.goodreads_id = goodreads_id

self.availabilities = []

def add_availablity(self, availability):
self.availabilities.append(availability)

if availability.is_available:
self.is_available = True

def get_cover_url(self):
if self.frabl is not None:
return "https://webservices.bibliotheek.be/index.php?func=cover&ISBN={0}&VLACCnr={1}&CDR=&EAN=&ISMN=&coversize=medium".format(self.isbn, self.vlacc)
else:
return self.goodreads_cover

def to_html(self):
content = """<div class="card">
<div class="row no-gutters">
<div class="col-md-1">"""
content += '<img class="card-img" src="{}">'.format(self.get_cover_url())

content += """</div>
<div class="col-md-11">
<div class="card-body">"""

content += '<h5 class="card-title">{}</h5>'.format(self.title)
content += '<h6 class="card-subtitle mb-2 text-muted">{}</h6>'.format(self.author)

if self.is_available and len(self.availabilities) > 0:

content += """<p class="card-text">
<table class="table">
<thead>
<tr>
<th scope="col">Branch</th>
<th scope="col">Library</th>
<th scope="col">Sublocation</th>
<th scope="col">Shelfmark</th>
<th scope="col">Publication</th>
</tr>
</thead>
<tbody>"""

availables = [avail for avail in self.availabilities if avail.is_available]
for avail in availables:
content += "<tr>"
content += "<td>{}</td>".format(avail.branch)
content += "<td>{}</td>".format(avail.library)
content += "<td>"
if avail.zizo_image_url is not None:
content += "<img src='{}' style='width: 30px; margin-right: 5px;'/>".format(avail.zizo_image_url)
content += "{}</td>".format(avail.subloc)
content += "<td>{}</td>".format(avail.shelfmark)
content += "<td>{}</td>".format(avail.publication)
content += "<tr>"

content += """</tbody></table></p>"""

content += '<p class="card-text"><small class="text-muted">{} - {}</small></p>'.format(self.isbn, self.pages)
content += '<a href="{}" class="card-link" target="_blank">Catalogus</a>'.format(self.detail_page)
content += '<a href="{}" class="card-link" target="_blank">Goodreads</a>'.format(self.goodreads_page)

content += """</div>
</div>
</div>
</div>"""

elif not self.is_available and len(self.availabilities) > 0:

content += """<p class="card-text">
<table class="table">
<thead>
<tr>
<th scope="col">Branch</th>
<th scope="col">Library</th>
<th scope="col">Status</th>
<th scope="col">Return Date</th>
<th scope="col">Days until available</th>
</tr>
</thead>
<tbody>"""

unavailables = [avail for avail in self.availabilities if avail.is_available == False]
for unavail in unavailables:
content += "<tr>"
content += "<td>{}</td>".format(unavail.branch)
content += "<td>{}</td>".format(unavail.library)
content += "<td>{}</td>".format(unavail.status)
content += "<td>{}</td>".format(unavail.return_date)
if unavail.return_date is not None:
content += "<td>{}</td>".format((unavail.return_date - date.today()).days)
content += "<tr>"

content += """</tbody></table></p>"""

content += '<p class="card-text"><small class="text-muted">{} - {}</small></p>'.format(self.isbn, self.pages)
content += '<a href="{}" class="card-link" target="_blank">Catalogus</a>'.format(self.detail_page)
content += '<a href="{}" class="card-link" target="_blank">Goodreads</a>'.format(self.goodreads_page)

content += """</div>
</div>
</div>
</div>"""

elif self.frabl is not None:
content += "<p class='card-text'>No availabilities found.</p>"

content += '<p class="card-text"><small class="text-muted">{} - {}</small></p>'.format(self.isbn, self.pages)
content += '<a href="{}" class="card-link" target="_blank">Catalogus</a>'.format(self.detail_page)
content += '<a href="{}" class="card-link" target="_blank">Goodreads</a>'.format(self.goodreads_page)

content += """</div>
</div>
</div>
</div>"""
else:
content += "<p class='card-text'>Not found in library catalogue.</p>"
content += '<a href="{}" class="card-link" target="_blank">Goodreads</a>'.format(self.goodreads_page)

content += """</div>
</div>
</div>
</div>"""

return content
67 changes: 67 additions & 0 deletions src/Cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import json
import os
from src.utils.Singleton import Singleton
from src.Book import Book
from src.Catalogue import Catalogue

class Cache(Singleton, object):

catalogue = None
tokens = None

cache_dir = os.path.join(os.path.dirname(__file__), '..\\cache')
tokens_cache_path = os.path.join(os.path.dirname(__file__), '..\\cache\\.tokens')
catalogue_cache_path = os.path.join(os.path.dirname(__file__), '..\\cache\\.catalogue.json')

def __init__(self):
if not os.path.exists(self.cache_dir):
os.mkdir(self.cache_dir)

def load_catalogue(self):
if os.path.exists(self.catalogue_cache_path):
with open(self.catalogue_cache_path) as catalogue_cache:
self.catalogue = json.load(catalogue_cache)

def save_catalogue(self, catalogue: Catalogue):
with open(self.catalogue_cache_path, 'w') as catalogue_cache:
dicts = [book.__dict__ for book in catalogue.books if book.frabl is not None]
catalogue_cache.write(json.dumps(dicts))

self.catalogue = dicts

def get_book(self, goodreads_id: str):
if self.catalogue is None:
return None

found_book = next((book for book in self.catalogue if book['goodreads_id'] == goodreads_id), None)
if found_book is None:
return None

book = Book(found_book['author'], found_book['title'], found_book['goodreads_id'])
book.frabl = found_book['frabl']
book.vlacc = found_book['vlacc']
book.isbn = found_book['isbn']
book.pages = found_book['pages']
book.detail_page = found_book['detail_page']
book.goodreads_page = found_book['goodreads_page']
book.goodreads_cover = found_book['goodreads_cover']

print('{} by {} found in cache'.format(book.title, book.author))

return book

def load_tokens(self):
if os.path.exists(self.tokens_cache_path):
with open(self.tokens_cache_path) as tokens_cache:
self.tokens = json.load(tokens_cache)

return self.tokens

return None

def save_access_tokens(self, access_token: str, access_token_secret: str):
with open(self.tokens_cache_path, 'w') as token_file:
token_file.write(json.dumps({
'token': access_token,
'secret': access_token_secret
}))
20 changes: 20 additions & 0 deletions src/Catalogue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from src.utils.Singleton import Singleton

class Catalogue(Singleton, object):

books = None

def set_books(self, books: list):
self.books = books

def get_available_books(self):
return [book for book in self.books if book.is_available]

def get_unavailable_books(self):
return [book for book in self.books if not book.is_available and len(book.availabilities) > 0]

def get_books_with_no_availables(self):
return [book for book in self.books if not book.is_available and len(book.availabilities) == 0 and book.frabl is not None]

def get_not_found_books(self):
return [book for book in self.books if book.frabl is None]
Loading

0 comments on commit 553d6e2

Please sign in to comment.