Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
gravio-community committed Feb 7, 2021
0 parents commit 3a9b17a
Show file tree
Hide file tree
Showing 38 changed files with 74,167 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
SPOTIPY_CLIENT_ID=clientid
SPOTIPY_CLIENT_SECRET=clientsecret
SPOTIPY_REDIRECT_URI=callback
RADIO_REFRESH_TOKEN=token
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
venv
*.pyc
staticfiles
.env
db.sqlite3
getting-started/*
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: gunicorn gettingstarted.wsgi --log-file -
1 change: 1 addition & 0 deletions Procfile.windows
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: python manage.py runserver 0.0.0.0:7000
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Python: Getting Started

A barebones Django app, which can easily be deployed to Heroku.

This application supports the [Getting Started with Python on Heroku](https://devcenter.heroku.com/articles/getting-started-with-python) article - check it out.

## Running Locally

Make sure you have Python 3.7 [installed locally](http://install.python-guide.org). To push to Heroku, you'll need to install the [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli), as well as [Postgres](https://devcenter.heroku.com/articles/heroku-postgresql#local-setup).

```sh
$ git clone https://github.com/heroku/python-getting-started.git
$ cd python-getting-started

$ python3 -m venv getting-started
$ pip install -r requirements.txt

$ createdb python_getting_started

$ python manage.py migrate
$ python manage.py collectstatic

$ heroku local
```

Your app should now be running on [localhost:5000](http://localhost:5000/).

## Deploying to Heroku

```sh
$ heroku create
$ git push heroku main

$ heroku run python manage.py migrate
$ heroku open
```
or

[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy)

## Documentation

For more information about using Python on Heroku, see these Dev Center articles:

- [Python on Heroku](https://devcenter.heroku.com/categories/python)
Empty file added api/__init__.py
Empty file.
161 changes: 161 additions & 0 deletions api/currently_playing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
from datetime import datetime, timedelta
import time
import api.spotify as spotify
import hello.models
import json

Currently_Playing = None
Kind = None
Progress_Ms = 0
Duration_Ms = 0
Is_Playing = False
PlayedAt = 0 # timestamp in ms


# Datetime of the last check of Spotify
Last_Spotify_Check = datetime.min
Check_Every = timedelta(seconds=10)

# because history can be a bit finnicky, we add some extra checks before adding to history
# check most recent history song. If same as current song...
# check if (played_at + duration_ms) <= now
# if so, don't add. Because The old song couldn't finished playing, and its the same song, so just ignore it.
def check_add_history(song, timestamp, duration_ms):
h = hello.models.History.objects.order_by("-Timestamp").first()
if h is not None:
if h.Timestamp == timestamp:
print(f"Not adding {song.Id} to history because of a Timestamp conflict.")
return
if h.Song.Id == song.Id:
# most recent history item is the same as this one, so check the time info
td = timedelta(milliseconds=duration_ms)
if (h.Timestamp + td) >= datetime.now():
print(f"Not adding {song.Id} to history because it didn't pass the history time-check")
return
h = hello.models.History.objects.create(Timestamp=timestamp, Song=song)
h.save()
return


# Tries to add the Spotify song to the database, and place it in the history table too
# Skips adding to history if the Played AT timestamp was already seen
def add_spotify_to_history(spot):
played = datetime.fromtimestamp(int(spot["timestamp"] / 1000))
songId = spot["item"]["id"]
jdump = json.dumps(spot["item"])
# first try to add the basic song info to the Songs table
msong = song_get_or_create(songId, "spotify", jdump)
# next, try to add it to the History table
check_add_history(msong, played, spot["item"]["duration_ms"])
return

def add_song(obj):
global Currently_Playing, Kind, Progress_Ms, Duration_Ms, PlayedAt, Is_Playing
if obj["kind"] == "youtube":
ytPlayed = int(obj["timestamp"])
ytId = obj["item"]["id"]
ytPlaying = obj["is_playing"]

oldId = Currently_Playing["item"]["id"] if Currently_Playing is not None else ""

if Kind == "spotify" and not ytPlaying:
print("Tried to replace a spotify track with a youtube one, but youtube was paused. Skipping")
return
# only set PlayedAt if the song actually changed.
if oldId != ytId:
PlayedAt = ytPlayed
played = datetime.fromtimestamp(ytPlayed)
jdump = json.dumps(obj["item"])
Is_Playing = ytPlaying
Kind = "youtube"
Duration_Ms = obj["item"]["duration_ms"]
Progress_Ms = obj["progress_ms"]
Currently_Playing = obj
msong = song_get_or_create(ytId, "youtube", jdump)
# only add to history if we actually play the video and its a different song
if ytPlaying and oldId != ytId:
check_add_history(msong, played, obj["item"]["duration_ms"])
return

def song_get_or_create(id, kind, jdata):
song = hello.models.Song.objects.filter(Id=id, Kind=kind).first()
if not song:
song = hello.models.Song.objects.create(Id=id, Kind=kind, JsonData=jdata)
return song

def get_currently_playing() -> int:
global Currently_Playing, Kind, Progress_Ms, Duration_Ms, PlayedAt, Last_Spotify_Check, Is_Playing
now = datetime.now()
# Prioritize returning any currently playing Spotify song
if (Currently_Playing is None) or ((Last_Spotify_Check + Check_Every) <= now):
print("Spotify hasn't been checked recently...")
song_spot = spotify.get_currently_playing()
if song_spot is not None:
# only override the currently playing if the received song is playing. if it is paused, return currently playing
if Kind == "spotify" or song_spot["is_playing"]:
Is_Playing = song_spot["is_playing"]
add_spotify_to_history(song_spot)
print("Spotify was playing something. It has Priority.")
Currently_Playing = song_spot
Kind = "spotify"
Last_Spotify_Check = now
PlayedAt = int(song_spot["timestamp"] / 1000) # Spotify gives us something that Datetime cant work with
print("played at: " + datetime.fromtimestamp(PlayedAt).strftime("%I:%M:%S"))
Progress_Ms = song_spot["progress_ms"]
Duration_Ms = song_spot["item"]["duration_ms"]
return {
"song": song_spot,
"kind": "spotify"
}
elif not song_spot["is_playing"]:
print("Got a spotify song, but it wasn't playing. Returning currently playing instead.")

# Next, check if our currently playing song is still valid (i.e., a youtube video)
if Currently_Playing is not None:
dtPlayedat = datetime.fromtimestamp(PlayedAt)
tdelta = timedelta(milliseconds=(Duration_Ms))
expiry = dtPlayedat + tdelta
if expiry >= now: # we may come into the video halfway through, so subtract our progress
# song is still valid
print("Currently Playing is still valid")
cp = Currently_Playing
if Kind != "spotify":
if Is_Playing:
cp["progress_ms"] = cp["progress_ms"] + int((datetime.now() - datetime.fromtimestamp(PlayedAt)).total_seconds())
return {
"song": cp,
"kind": Kind,
}
else:
if Progress_Ms < Duration_Ms and Is_Playing:
print("Song should be expired, but user seems to have gone backwards on it.")
return {
"song": Currently_Playing,
"kind": Kind,
}
else:
print("datetime playedat: " + dtPlayedat.strftime("%I:%M:%S"))
print("timedelta (dms - progms): " + str(timedelta(milliseconds=(Duration_Ms - Progress_Ms))))
print("Time to expiry: " + expiry.strftime("%I:%M:%S"))
print("Currently playing song has expired")

print("Radio NF is offline - nothing from youtube, currently_playing, or Spotify")
return None

# NF TODO what is up with signing shit?

def get_history(previous=50):
hist = []
skip = 0
if Currently_Playing is not None:
skip = 1
for history in hello.models.History.objects.all().order_by("-Timestamp")[skip:previous]: # skip most recent
h = {
"timestamp": int(time.mktime(history.Timestamp.timetuple())),
"song": {
"kind": history.Song.Kind,
"item": json.loads(history.Song.JsonData)
}
}
hist.append(h)
return hist
58 changes: 58 additions & 0 deletions api/spotify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import spotipy
from spotipy.oauth2 import SpotifyOAuth

scope = "user-library-read user-read-playback-state user-read-currently-playing user-read-recently-played user-top-read user-read-playback-position"

sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))

__default_image_icon = "https://community.spotify.com/t5/image/serverpage/image-id/55829iC2AD64ADB887E2A5/image-size/large?v=1.0&px=999"
def get_currently_playing():
song = sp.current_user_playing_track()
if song is None:
print("User is not currently playing any tracks")
return None
else:
si = song["item"]
item = {
"id": si["id"],
"name": si["name"],
"is_local": si["is_local"],
"href": si["href"],
"preview_url": si["preview_url"],
"duration_ms": si["duration_ms"],
"album": si["album"],
"artists": si["artists"],
"main_image": __default_image_icon,
"main_link": None
}


d = {
"timestamp": song["timestamp"],
"progress_ms": song["progress_ms"],
"is_playing": song["is_playing"],
"item": item,
}

if not si["is_local"]:
a = si["album"]
item["main_image"] = a["images"][0]["url"]
item["main_link"] = si["external_urls"]["spotify"]
item["album"] = {
"id": a["id"],
"images": a["images"],
"name": a["name"],
}
item["artists"] = [
{"id": x["id"], "name": x["name"], "href":x["href"]} for x in si["artists"]
]
else:
item["id"] = f"{item['name']}{str(item['duration_ms'])}" # default is local id

return d


# results = sp.current_user_saved_tracks()
# for idx, item in enumerate(results['items']):
# track = item['track']
# print(idx, track['artists'][0]['name'], " – ", track['name'])
22 changes: 22 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "Start on Heroku: Python",
"description": "A barebones Python app, which can easily be deployed to Heroku.",
"image": "heroku/python",
"repository": "https://github.com/heroku/python-getting-started",
"keywords": ["python", "django" ],
"addons": [ "heroku-postgresql" ],
"env": {
"SECRET_KEY": {
"description": "The secret key for the Django application.",
"generator": "secret"
}
},
"environments": {
"test": {
"scripts": {
"test-setup": "python manage.py collectstatic --noinput",
"test": "python manage.py test"
}
}
}
}
Loading

0 comments on commit 3a9b17a

Please sign in to comment.