mirror of
https://github.com/spl0k/supysonic.git
synced 2024-11-10 04:02:17 +00:00
Create and use PodcastStatus enum
Add missing foreign keys Require url for podcast channel be unique Add basic url validation Use soft delete for channels and episodes to match Subsonic impl
This commit is contained in:
parent
dd4614d735
commit
08a83a8492
1
setup.py
1
setup.py
@ -22,6 +22,7 @@ reqs = [
|
|||||||
"mutagen>=1.33",
|
"mutagen>=1.33",
|
||||||
"watchdog>=0.8.0",
|
"watchdog>=0.8.0",
|
||||||
"zipstream",
|
"zipstream",
|
||||||
|
"feedparser",
|
||||||
]
|
]
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
|
@ -7,9 +7,15 @@
|
|||||||
#
|
#
|
||||||
# Distributed under terms of the GNU AGPLv3 license.
|
# Distributed under terms of the GNU AGPLv3 license.
|
||||||
|
|
||||||
from flask import request
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from time import mktime
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from ..db import PodcastChannel, PodcastEpisode
|
from flask import request
|
||||||
|
import feedparser
|
||||||
|
|
||||||
|
from ..db import PodcastChannel, PodcastEpisode, PodcastStatus
|
||||||
|
|
||||||
from . import api, get_entity, require_podcast
|
from . import api, get_entity, require_podcast
|
||||||
from .exceptions import Forbidden, MissingParameter, NotFound
|
from .exceptions import Forbidden, MissingParameter, NotFound
|
||||||
@ -22,7 +28,9 @@ def get_podcasts():
|
|||||||
if channel_id:
|
if channel_id:
|
||||||
channels = (get_entity(PodcastChannel),)
|
channels = (get_entity(PodcastChannel),)
|
||||||
else:
|
else:
|
||||||
channels = PodcastChannel.select().sort_by(PodcastChannel.url)
|
channels = PodcastChannel \
|
||||||
|
.select(lambda chan: chan.status != PodcastStatus.deleted.value) \
|
||||||
|
.sort_by(PodcastChannel.url)
|
||||||
|
|
||||||
return request.formatter(
|
return request.formatter(
|
||||||
"podcasts",
|
"podcasts",
|
||||||
@ -34,8 +42,26 @@ def get_podcasts():
|
|||||||
@require_podcast
|
@require_podcast
|
||||||
def create_podcast_channel():
|
def create_podcast_channel():
|
||||||
url = request.values["url"]
|
url = request.values["url"]
|
||||||
|
parsed_url = urlparse(url)
|
||||||
|
has_scheme_and_location = parsed_url.scheme and (parsed_url.netloc or parsed_url.path)
|
||||||
|
if not has_scheme_and_location:
|
||||||
|
return request.formatter.error(10, 'unexepected url')
|
||||||
|
|
||||||
PodcastChannel(url=url)
|
feed = feedparser.parse(url)
|
||||||
|
channel = PodcastChannel(
|
||||||
|
url=url,
|
||||||
|
title=feed.feed.title,
|
||||||
|
)
|
||||||
|
|
||||||
|
for item in feed.entries:
|
||||||
|
channel.episodes.create(
|
||||||
|
title=item.title,
|
||||||
|
description=item.description,
|
||||||
|
stream_url=item.links[0].href,
|
||||||
|
duration=item.links[0].length,
|
||||||
|
publish_date=datetime.fromtimestamp(mktime(item.published_parsed)),
|
||||||
|
status=PodcastStatus.new.value,
|
||||||
|
)
|
||||||
|
|
||||||
return request.formatter.empty
|
return request.formatter.empty
|
||||||
|
|
||||||
@ -44,7 +70,7 @@ def create_podcast_channel():
|
|||||||
@require_podcast
|
@require_podcast
|
||||||
def delete_podcast_channel():
|
def delete_podcast_channel():
|
||||||
res = get_entity(PodcastChannel)
|
res = get_entity(PodcastChannel)
|
||||||
res.delete()
|
res.soft_delete()
|
||||||
|
|
||||||
return request.formatter.empty
|
return request.formatter.empty
|
||||||
|
|
||||||
@ -53,8 +79,7 @@ def delete_podcast_channel():
|
|||||||
@require_podcast
|
@require_podcast
|
||||||
def delete_podcast_episode():
|
def delete_podcast_episode():
|
||||||
res = get_entity(PodcastEpisode)
|
res = get_entity(PodcastEpisode)
|
||||||
res.delete()
|
res.soft_delete()
|
||||||
|
|
||||||
return request.formatter.empty
|
return request.formatter.empty
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ import pkg_resources
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from enum import Enum, unique
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
from pony.orm import Database, Required, Optional, Set, PrimaryKey, LongStr
|
from pony.orm import Database, Required, Optional, Set, PrimaryKey, LongStr
|
||||||
from pony.orm import ObjectNotFound, DatabaseError
|
from pony.orm import ObjectNotFound, DatabaseError
|
||||||
@ -573,32 +574,54 @@ class RadioStation(db.Entity):
|
|||||||
return info
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
@unique
|
||||||
|
class PodcastStatus(Enum):
|
||||||
|
new = 1
|
||||||
|
downloading = 2
|
||||||
|
completed = 3
|
||||||
|
error = 4
|
||||||
|
deleted = 5
|
||||||
|
skipped = 6
|
||||||
|
|
||||||
|
|
||||||
class PodcastChannel(db.Entity):
|
class PodcastChannel(db.Entity):
|
||||||
_table_ = "podcast_channel"
|
_table_ = "podcast_channel"
|
||||||
|
|
||||||
id = PrimaryKey(UUID, default=uuid4)
|
id = PrimaryKey(UUID, default=uuid4)
|
||||||
url = Required(str)
|
url = Required(str, unique=True)
|
||||||
title = Optional(str)
|
title = Optional(str, nullable=True)
|
||||||
description = Optional(str)
|
description = Optional(str, nullable=True)
|
||||||
cover_art = Optional(str)
|
cover_art = Optional(str, nullable=True)
|
||||||
original_image_url = Optional(str)
|
original_image_url = Optional(str)
|
||||||
status = Required(str, default="new")
|
# Status params mirror PodcastStatus
|
||||||
error_message = Optional(str)
|
status = Required(int, min=1, max=6, default=1)
|
||||||
|
error_message = Optional(str, nullable=True)
|
||||||
created = Required(datetime, precision=0, default=now)
|
created = Required(datetime, precision=0, default=now)
|
||||||
last_fetched = Optional(datetime, precision=0)
|
last_fetched = Optional(datetime, precision=0, nullable=True)
|
||||||
episodes = Set(lambda: PodcastEpisode, lazy=True)
|
episodes = Set(lambda: PodcastEpisode, lazy=True)
|
||||||
|
|
||||||
|
def soft_delete(self):
|
||||||
|
for ep in self.episodes:
|
||||||
|
ep.soft_delete()
|
||||||
|
|
||||||
|
self.status = PodcastStatus.deleted.value
|
||||||
|
self.error_message = None
|
||||||
|
|
||||||
def as_subsonic_channel(self, include_episodes=False):
|
def as_subsonic_channel(self, include_episodes=False):
|
||||||
info = dict(
|
info = dict(
|
||||||
id=self.id,
|
id=self.id,
|
||||||
url=self.url,
|
url=self.url,
|
||||||
title=self.title,
|
status=PodcastStatus(self.status).name,
|
||||||
description=self.description,
|
|
||||||
status=self.status,
|
|
||||||
errorMessage=self.error_message,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.title:
|
||||||
|
info["title"] = self.title
|
||||||
|
if self.description:
|
||||||
|
info["description"] = self.description
|
||||||
|
if self.error_message:
|
||||||
|
info["errorMessage"] = self.error_message
|
||||||
|
|
||||||
if include_episodes:
|
if include_episodes:
|
||||||
self.episodes.load()
|
|
||||||
info["episode"] = [ep.as_subsonic_episode() for ep in self.episodes]
|
info["episode"] = [ep.as_subsonic_episode() for ep in self.episodes]
|
||||||
return info
|
return info
|
||||||
|
|
||||||
@ -608,25 +631,40 @@ class PodcastEpisode(db.Entity):
|
|||||||
|
|
||||||
id = PrimaryKey(UUID, default=uuid4)
|
id = PrimaryKey(UUID, default=uuid4)
|
||||||
channel = Required(PodcastChannel, column="channel_id")
|
channel = Required(PodcastChannel, column="channel_id")
|
||||||
stream_url = Optional(str)
|
# Location of the episode. Used to stream and download.
|
||||||
file_path = Optional(str)
|
stream_url = Required(str)
|
||||||
title = Optional(str)
|
# Path of file after it has been downloaded.
|
||||||
description = Optional(str)
|
file_path = Optional(str, nullable=True)
|
||||||
duration = Optional(str)
|
title = Required(str)
|
||||||
status = Required(str, default="new")
|
description = Optional(str, nullable=True)
|
||||||
|
duration = Optional(str, nullable=True)
|
||||||
|
# Status params mirror PodcastStatus
|
||||||
|
status = Required(int, min=1, max=6, default=1)
|
||||||
publish_date = Optional(datetime, precision=0, default=now)
|
publish_date = Optional(datetime, precision=0, default=now)
|
||||||
created = Required(datetime, precision=0, default=now)
|
created = Required(datetime, precision=0, default=now)
|
||||||
|
|
||||||
|
def soft_delete(self):
|
||||||
|
self.status = PodcastStatus.deleted.value
|
||||||
|
|
||||||
|
if self.file_path:
|
||||||
|
if os.path.exists(self.file_path):
|
||||||
|
os.remove(self.file_path)
|
||||||
|
self.file_path = None
|
||||||
|
|
||||||
def as_subsonic_episode(self):
|
def as_subsonic_episode(self):
|
||||||
info = dict(
|
info = dict(
|
||||||
id=self.id,
|
id=self.id,
|
||||||
isDir=False,
|
isDir=False,
|
||||||
streamId="podcast:{}".format(self.id),
|
|
||||||
title=self.title,
|
title=self.title,
|
||||||
description=self.description,
|
streamId="podcast:{}".format(self.id),
|
||||||
status=self.status,
|
status=PodcastStatus(self.status).name,
|
||||||
publishDate=self.publish_date.isoformat(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.description:
|
||||||
|
info["description"] = self.description,
|
||||||
|
if self.publish_date:
|
||||||
|
info["publishDate"] = self.publish_date.isoformat()
|
||||||
|
|
||||||
return info
|
return info
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,12 +2,12 @@ ALTER TABLE user ADD podcast BOOLEAN DEFAULT false NOT NULL;
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS podcast_channel (
|
CREATE TABLE IF NOT EXISTS podcast_channel (
|
||||||
id BINARY(16) PRIMARY KEY,
|
id BINARY(16) PRIMARY KEY,
|
||||||
url VARCHAR(256) NOT NULL,
|
url VARCHAR(256) UNIQUE NOT NULL,
|
||||||
title VARCHAR(256),
|
title VARCHAR(256),
|
||||||
description VARCHAR(256),
|
description VARCHAR(256),
|
||||||
cover_art VARCHAR(256),
|
cover_art VARCHAR(256),
|
||||||
original_image_url VARCHAR(256),
|
original_image_url VARCHAR(256),
|
||||||
status VARCHAR(16),
|
status TINYINT NOT NULL,
|
||||||
error_message VARCHAR(256),
|
error_message VARCHAR(256),
|
||||||
created TIMESTAMP NOT NULL,
|
created TIMESTAMP NOT NULL,
|
||||||
last_fetched TIMESTAMP
|
last_fetched TIMESTAMP
|
||||||
@ -15,15 +15,16 @@ CREATE TABLE IF NOT EXISTS podcast_channel (
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS podcast_episode (
|
CREATE TABLE IF NOT EXISTS podcast_episode (
|
||||||
id BINARY(16) PRIMARY KEY,
|
id BINARY(16) PRIMARY KEY,
|
||||||
stream_url VARCHAR(256),
|
stream_url VARCHAR(256) NOT NULL,
|
||||||
file_path VARCHAR(256),
|
file_path VARCHAR(256),
|
||||||
channel_id CHAR(36) NOT NULL,
|
channel_id CHAR(36) NOT NULL,
|
||||||
title VARCHAR(256),
|
title VARCHAR(256),
|
||||||
description VARCHAR(256),
|
description VARCHAR(256),
|
||||||
duration VARCHAR(8),
|
duration VARCHAR(8),
|
||||||
status VARCHAR(16) NOT NULL,
|
status TINYINT NOT NULL,
|
||||||
publish_date DATETIME,
|
publish_date DATETIME,
|
||||||
created DATETIME NOT NULL
|
created DATETIME NOT NULL,
|
||||||
FOREIGN KEY(channel_id) REFERENCES podcast_channel(id),
|
FOREIGN KEY(channel_id) REFERENCES podcast_channel(id),
|
||||||
INDEX index_episode_channel_id_fk (channel_id)
|
INDEX index_episode_channel_id_fk (channel_id),
|
||||||
|
INDEX index_episode_status (status)
|
||||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
@ -7,22 +7,24 @@ CREATE TABLE IF NOT EXISTS podcast_channel (
|
|||||||
description VARCHAR(256),
|
description VARCHAR(256),
|
||||||
cover_art VARCHAR(256),
|
cover_art VARCHAR(256),
|
||||||
original_image_url VARCHAR(256),
|
original_image_url VARCHAR(256),
|
||||||
status VARCHAR(16),
|
status TINYINT NOT NULL,
|
||||||
error_message VARCHAR(256),
|
error_message VARCHAR(256),
|
||||||
created TIMESTAMP NOT NULL,
|
created TIMESTAMP NOT NULL,
|
||||||
last_fetched TIMESTAMP
|
last_fetched TIMESTAMP
|
||||||
);
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS index_channel_status ON podcast_channel(status);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS podcast_episode (
|
CREATE TABLE IF NOT EXISTS podcast_episode (
|
||||||
id UUID PRIMARY KEY,
|
id UUID PRIMARY KEY,
|
||||||
stream_url VARCHAR(256),
|
stream_url VARCHAR(256) NOT NULL,
|
||||||
file_path VARCHAR(256),
|
file_path VARCHAR(256),
|
||||||
channel_id CHAR(36) NOT NULL REFERENCES podcast_channel(id),
|
channel_id CHAR(36) NOT NULL REFERENCES podcast_channel(id),
|
||||||
title VARCHAR(256),
|
title VARCHAR(256),
|
||||||
description VARCHAR(256),
|
description VARCHAR(256),
|
||||||
duration VARCHAR(8),
|
duration VARCHAR(8),
|
||||||
status VARCHAR(16) NOT NULL,
|
status TINYINT NOT NULL,
|
||||||
publish_date TIMESTAMP,
|
publish_date TIMESTAMP,
|
||||||
created TIMESTAMP NOT NULL
|
created TIMESTAMP NOT NULL
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS index_episode_channel_id_fk ON podcast_channel(id);
|
CREATE INDEX IF NOT EXISTS index_episode_channel_id_fk ON podcast_channel(id);
|
||||||
|
CREATE INDEX IF NOT EXISTS index_episode_status ON podcast_episode(status);
|
||||||
|
@ -2,27 +2,29 @@ ALTER TABLE user ADD podcast BOOLEAN DEFAULT false NOT NULL;
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS podcast_channel (
|
CREATE TABLE IF NOT EXISTS podcast_channel (
|
||||||
id CHAR(36) PRIMARY KEY,
|
id CHAR(36) PRIMARY KEY,
|
||||||
url VARCHAR(256) NOT NULL,
|
url VARCHAR(256) UNIQUE NOT NULL,
|
||||||
title VARCHAR(256),
|
title VARCHAR(256),
|
||||||
description VARCHAR(256),
|
description VARCHAR(256),
|
||||||
cover_art VARCHAR(256),
|
cover_art VARCHAR(256),
|
||||||
original_image_url VARCHAR(256),
|
original_image_url VARCHAR(256),
|
||||||
status VARCHAR(16),
|
status TINYINT NOT NULL,
|
||||||
error_message VARCHAR(256),
|
error_message VARCHAR(256),
|
||||||
created DATETIME NOT NULL,
|
created DATETIME NOT NULL,
|
||||||
last_fetched DATETIME
|
last_fetched DATETIME
|
||||||
);
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS index_channel_status_id_fk ON podcast_channel(status);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS podcast_episode (
|
CREATE TABLE IF NOT EXISTS podcast_episode (
|
||||||
id CHAR(36) PRIMARY KEY,
|
id CHAR(36) PRIMARY KEY,
|
||||||
stream_url VARCHAR(256),
|
stream_url VARCHAR(256) NOT NULL,
|
||||||
file_path VARCHAR(256),
|
file_path VARCHAR(256),
|
||||||
channel_id CHAR(36) NOT NULL,
|
channel_id CHAR(36) NOT NULL REFERENCES podcast_channel,
|
||||||
title VARCHAR(256),
|
title VARCHAR(256),
|
||||||
description VARCHAR(256),
|
description VARCHAR(256),
|
||||||
duration VARCHAR(8),
|
duration VARCHAR(8),
|
||||||
status VARCHAR(16) NOT NULL,
|
status TINYINT NOT NULL,
|
||||||
publish_date DATETIME,
|
publish_date DATETIME,
|
||||||
created DATETIME NOT NULL
|
created DATETIME NOT NULL
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS index_episode_channel_id_fk ON podcast_channel(id);
|
CREATE INDEX IF NOT EXISTS index_episode_channel_id_fk ON podcast_channel(id);
|
||||||
|
CREATE INDEX IF NOT EXISTS index_episode_status_id_fk ON podcast_episode(status);
|
||||||
|
@ -161,12 +161,12 @@ CREATE TABLE IF NOT EXISTS radio_station (
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS podcast_channel (
|
CREATE TABLE IF NOT EXISTS podcast_channel (
|
||||||
id BINARY(16) PRIMARY KEY,
|
id BINARY(16) PRIMARY KEY,
|
||||||
url VARCHAR(256) NOT NULL,
|
url VARCHAR(256) UNIQUE NOT NULL,
|
||||||
title VARCHAR(256),
|
title VARCHAR(256),
|
||||||
description VARCHAR(256),
|
description VARCHAR(256),
|
||||||
cover_art VARCHAR(256),
|
cover_art VARCHAR(256),
|
||||||
original_image_url VARCHAR(256),
|
original_image_url VARCHAR(256),
|
||||||
status VARCHAR(16),
|
status TINYINT NOT NULL,
|
||||||
error_message VARCHAR(256),
|
error_message VARCHAR(256),
|
||||||
created TIMESTAMP NOT NULL,
|
created TIMESTAMP NOT NULL,
|
||||||
last_fetched TIMESTAMP
|
last_fetched TIMESTAMP
|
||||||
@ -174,15 +174,16 @@ CREATE TABLE IF NOT EXISTS podcast_channel (
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS podcast_episode (
|
CREATE TABLE IF NOT EXISTS podcast_episode (
|
||||||
id BINARY(16) PRIMARY KEY,
|
id BINARY(16) PRIMARY KEY,
|
||||||
stream_url VARCHAR(256),
|
stream_url VARCHAR(256) NOT NULL,
|
||||||
file_path VARCHAR(256),
|
file_path VARCHAR(256),
|
||||||
channel_id CHAR(36) NOT NULL,
|
channel_id CHAR(36) NOT NULL,
|
||||||
title VARCHAR(256),
|
title VARCHAR(256),
|
||||||
description VARCHAR(256),
|
description VARCHAR(256),
|
||||||
duration VARCHAR(8),
|
duration VARCHAR(8),
|
||||||
status VARCHAR(16) NOT NULL,
|
status TINYINT NOT NULL,
|
||||||
publish_date DATETIME,
|
publish_date DATETIME,
|
||||||
created DATETIME NOT NULL,
|
created DATETIME NOT NULL,
|
||||||
FOREIGN KEY(channel_id) REFERENCES podcast_channel(id),
|
FOREIGN KEY(channel_id) REFERENCES podcast_channel(id),
|
||||||
INDEX index_episode_channel_id_fk (channel_id)
|
INDEX index_episode_channel_id_fk (channel_id),
|
||||||
|
INDEX index_episode_status (status)
|
||||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
@ -166,22 +166,24 @@ CREATE TABLE IF NOT EXISTS podcast_channel (
|
|||||||
description VARCHAR(256),
|
description VARCHAR(256),
|
||||||
cover_art VARCHAR(256),
|
cover_art VARCHAR(256),
|
||||||
original_image_url VARCHAR(256),
|
original_image_url VARCHAR(256),
|
||||||
status VARCHAR(16),
|
status TINYINT NOT NULL,
|
||||||
error_message VARCHAR(256),
|
error_message VARCHAR(256),
|
||||||
created TIMESTAMP NOT NULL,
|
created TIMESTAMP NOT NULL,
|
||||||
last_fetched TIMESTAMP
|
last_fetched TIMESTAMP
|
||||||
);
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS index_channel_status ON podcast_channel(status);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS podcast_episode (
|
CREATE TABLE IF NOT EXISTS podcast_episode (
|
||||||
id UUID PRIMARY KEY,
|
id UUID PRIMARY KEY,
|
||||||
stream_url VARCHAR(256),
|
stream_url VARCHAR(256) NOT NULL,
|
||||||
file_path VARCHAR(256),
|
file_path VARCHAR(256),
|
||||||
channel_id CHAR(36) NOT NULL REFERENCES podcast_channel(id),
|
channel_id CHAR(36) NOT NULL REFERENCES podcast_channel(id),
|
||||||
title VARCHAR(256),
|
title VARCHAR(256),
|
||||||
description VARCHAR(256),
|
description VARCHAR(256),
|
||||||
duration VARCHAR(8),
|
duration VARCHAR(8),
|
||||||
status VARCHAR(16) NOT NULL,
|
status TINYINT NOT NULL,
|
||||||
publish_date TIMESTAMP,
|
publish_date TIMESTAMP,
|
||||||
created TIMESTAMP NOT NULL
|
created TIMESTAMP NOT NULL
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS index_episode_channel_id_fk ON podcast_channel(id);
|
CREATE INDEX IF NOT EXISTS index_episode_channel_id_fk ON podcast_channel(id);
|
||||||
|
CREATE INDEX IF NOT EXISTS index_episode_status ON podcast_episode(status);
|
||||||
|
@ -163,27 +163,29 @@ CREATE TABLE IF NOT EXISTS radio_station (
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS podcast_channel (
|
CREATE TABLE IF NOT EXISTS podcast_channel (
|
||||||
id CHAR(36) PRIMARY KEY,
|
id CHAR(36) PRIMARY KEY,
|
||||||
url VARCHAR(256) NOT NULL,
|
url VARCHAR(256) UNIQUE NOT NULL,
|
||||||
title VARCHAR(256),
|
title VARCHAR(256),
|
||||||
description VARCHAR(256),
|
description VARCHAR(256),
|
||||||
cover_art VARCHAR(256),
|
cover_art VARCHAR(256),
|
||||||
original_image_url VARCHAR(256),
|
original_image_url VARCHAR(256),
|
||||||
status VARCHAR(16),
|
status TINYINT NOT NULL,
|
||||||
error_message VARCHAR(256),
|
error_message VARCHAR(256),
|
||||||
created DATETIME NOT NULL,
|
created DATETIME NOT NULL,
|
||||||
last_fetched DATETIME
|
last_fetched DATETIME
|
||||||
);
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS index_channel_status_id_fk ON podcast_channel(status);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS podcast_episode (
|
CREATE TABLE IF NOT EXISTS podcast_episode (
|
||||||
id CHAR(36) PRIMARY KEY,
|
id CHAR(36) PRIMARY KEY,
|
||||||
stream_url VARCHAR(256),
|
stream_url VARCHAR(256) NOT NULL,
|
||||||
file_path VARCHAR(256),
|
file_path VARCHAR(256),
|
||||||
channel_id CHAR(36) NOT NULL,
|
channel_id CHAR(36) NOT NULL REFERENCES podcast_channel,
|
||||||
title VARCHAR(256),
|
title VARCHAR(256),
|
||||||
description VARCHAR(256),
|
description VARCHAR(256),
|
||||||
duration VARCHAR(8),
|
duration VARCHAR(8),
|
||||||
status VARCHAR(16) NOT NULL,
|
status TINYINT NOT NULL,
|
||||||
publish_date DATETIME,
|
publish_date DATETIME,
|
||||||
created DATETIME NOT NULL
|
created DATETIME NOT NULL
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS index_episode_channel_id_fk ON podcast_channel(id);
|
CREATE INDEX IF NOT EXISTS index_episode_channel_id_fk ON podcast_channel(id);
|
||||||
|
CREATE INDEX IF NOT EXISTS index_episode_status_id_fk ON podcast_episode(status);
|
||||||
|
@ -8,11 +8,12 @@
|
|||||||
#
|
#
|
||||||
# Distributed under terms of the GNU AGPLv3 license.
|
# Distributed under terms of the GNU AGPLv3 license.
|
||||||
|
|
||||||
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from pony.orm import db_session
|
from pony.orm import db_session
|
||||||
|
|
||||||
from supysonic.db import PodcastChannel, PodcastEpisode
|
from supysonic.db import PodcastChannel, PodcastEpisode, PodcastStatus
|
||||||
|
|
||||||
from unittest import skip
|
from unittest import skip
|
||||||
|
|
||||||
@ -29,11 +30,13 @@ class PodcastTestCase(ApiTestBase):
|
|||||||
def assertDbCountEqual(self, entity, count):
|
def assertDbCountEqual(self, entity, count):
|
||||||
self.assertEqual(entity.select().count(), count)
|
self.assertEqual(entity.select().count(), count)
|
||||||
|
|
||||||
def assertPodcastChannelEquals(self, channel, url, status, title='', description='', error_message=''):
|
def assertPodcastChannelEquals(self, channel, url, status, title=None, description=None, error_message=None):
|
||||||
self.assertEqual(channel.url, url)
|
self.assertEqual(channel.url, url)
|
||||||
self.assertEqual(channel.title, title)
|
|
||||||
self.assertEqual(channel.description, description)
|
|
||||||
self.assertEqual(channel.status, status)
|
self.assertEqual(channel.status, status)
|
||||||
|
if title:
|
||||||
|
self.assertEqual(channel.title, title)
|
||||||
|
if description:
|
||||||
|
self.assertEqual(channel.description, description)
|
||||||
self.assertEqual(channel.error_message, error_message)
|
self.assertEqual(channel.error_message, error_message)
|
||||||
|
|
||||||
def test_create_podcast_channel(self):
|
def test_create_podcast_channel(self):
|
||||||
@ -46,18 +49,19 @@ class PodcastTestCase(ApiTestBase):
|
|||||||
|
|
||||||
# check params
|
# check params
|
||||||
self._make_request("createPodcastChannel", error=10)
|
self._make_request("createPodcastChannel", error=10)
|
||||||
|
self._make_request("createPodcastChannel", {"url": "bad url"}, error=10)
|
||||||
|
|
||||||
# create w/ required fields
|
# create w/ required fields
|
||||||
url = "https://example.local/podcast_channel/create"
|
url = "file://" + os.path.join(os.path.dirname(__file__), "../fixtures/rssfeed.xml")
|
||||||
|
self._make_request("createPodcastChannel", {"url": url}, skip_post=True)
|
||||||
|
|
||||||
self._make_request("createPodcastChannel", {"url": url})
|
self.assertDbCountEqual(PodcastChannel, 1)
|
||||||
|
self.assertDbCountEqual(PodcastEpisode, 20)
|
||||||
# the correct value is 2 because _make_request uses GET then POST
|
|
||||||
self.assertDbCountEqual(PodcastChannel, 2)
|
|
||||||
|
|
||||||
with db_session:
|
with db_session:
|
||||||
for channel in PodcastChannel.select():
|
self.assertPodcastChannelEquals(PodcastChannel.select().first(), url, PodcastStatus.new.value)
|
||||||
self.assertPodcastChannelEquals(channel, url, "new")
|
for episode in PodcastEpisode.select():
|
||||||
|
self.assertEqual(episode.status, PodcastStatus.new.value)
|
||||||
|
|
||||||
|
|
||||||
def test_delete_podcast_channel(self):
|
def test_delete_podcast_channel(self):
|
||||||
@ -77,13 +81,17 @@ class PodcastTestCase(ApiTestBase):
|
|||||||
with db_session:
|
with db_session:
|
||||||
channel = PodcastChannel(
|
channel = PodcastChannel(
|
||||||
url="https://example.local/podcast/delete",
|
url="https://example.local/podcast/delete",
|
||||||
status="new",
|
status=PodcastStatus.new.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._make_request("deletePodcastChannel", {"id": channel.id}, skip_post=True)
|
self._make_request("deletePodcastChannel", {"id": channel.id}, skip_post=True)
|
||||||
|
|
||||||
self.assertDbCountEqual(PodcastChannel, 0)
|
self.assertDbCountEqual(PodcastChannel, 1)
|
||||||
|
|
||||||
|
with db_session:
|
||||||
|
self.assertEqual(PodcastStatus.deleted.value, PodcastChannel[channel.id].status)
|
||||||
|
|
||||||
|
@db_session
|
||||||
def test_delete_podcast_episode(self):
|
def test_delete_podcast_episode(self):
|
||||||
# test for non-admin access
|
# test for non-admin access
|
||||||
self._make_request(
|
self._make_request(
|
||||||
@ -98,18 +106,19 @@ class PodcastTestCase(ApiTestBase):
|
|||||||
self._make_request("deletePodcastEpisode", {"id": str(uuid.uuid4())}, error=70)
|
self._make_request("deletePodcastEpisode", {"id": str(uuid.uuid4())}, error=70)
|
||||||
|
|
||||||
# delete
|
# delete
|
||||||
with db_session:
|
|
||||||
channel = PodcastChannel(
|
channel = PodcastChannel(
|
||||||
url="https://example.local/episode/delete",
|
url="https://example.local/episode/delete",
|
||||||
status="new",
|
status=PodcastStatus.new.value,
|
||||||
)
|
)
|
||||||
episode = channel.episodes.create(
|
episode = channel.episodes.create(
|
||||||
description="Test Episode 1",
|
title="Test Episode 1",
|
||||||
status="new",
|
stream_url="https://supysonic.local/delete/1",
|
||||||
|
status=PodcastStatus.new.value,
|
||||||
)
|
)
|
||||||
channel.episodes.create(
|
channel.episodes.create(
|
||||||
description="Test Episode 2",
|
title="Test Episode 2",
|
||||||
status="new",
|
stream_url="https://supysonic.local/delete/2",
|
||||||
|
status=PodcastStatus.new.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
# validate starting condition
|
# validate starting condition
|
||||||
@ -117,12 +126,18 @@ class PodcastTestCase(ApiTestBase):
|
|||||||
|
|
||||||
# validate delete of an episode
|
# validate delete of an episode
|
||||||
self._make_request("deletePodcastEpisode", {"id": episode.id}, skip_post=True)
|
self._make_request("deletePodcastEpisode", {"id": episode.id}, skip_post=True)
|
||||||
self.assertDbCountEqual(PodcastEpisode, 1)
|
## marked as deleted
|
||||||
|
self.assertDbCountEqual(PodcastEpisode, 2)
|
||||||
|
self.assertEqual(PodcastStatus.deleted.value, PodcastEpisode[episode.id].status)
|
||||||
|
|
||||||
# test for cascading delete on PodcastChannel
|
# test for cascading delete on PodcastChannel
|
||||||
self._make_request("deletePodcastChannel", {"id": channel.id}, skip_post=True)
|
self._make_request("deletePodcastChannel", {"id": channel.id}, skip_post=True)
|
||||||
self.assertDbCountEqual(PodcastChannel, 0)
|
## counts are the same but the status is now "deleted"
|
||||||
self.assertDbCountEqual(PodcastEpisode, 0)
|
self.assertDbCountEqual(PodcastChannel, 1)
|
||||||
|
self.assertEqual(PodcastStatus.deleted.value, PodcastChannel[channel.id].status)
|
||||||
|
self.assertDbCountEqual(PodcastEpisode, 2)
|
||||||
|
for ep in PodcastEpisode.select():
|
||||||
|
self.assertEqual(PodcastStatus.deleted.value, ep.status)
|
||||||
|
|
||||||
def test_get_podcasts(self):
|
def test_get_podcasts(self):
|
||||||
test_range = 3
|
test_range = 3
|
||||||
@ -130,10 +145,13 @@ class PodcastTestCase(ApiTestBase):
|
|||||||
for x in range(test_range):
|
for x in range(test_range):
|
||||||
ch = PodcastChannel(
|
ch = PodcastChannel(
|
||||||
url="https://example.local/podcast-{}".format(x),
|
url="https://example.local/podcast-{}".format(x),
|
||||||
status="new",
|
status=PodcastStatus.new.value,
|
||||||
)
|
)
|
||||||
for y in range(x + 1):
|
for y in range(x + 1):
|
||||||
ch.episodes.create(description="episode {} for channel {}".format(y, x))
|
ch.episodes.create(
|
||||||
|
title="episode {} for channel {}".format(y, x),
|
||||||
|
stream_url="https://supysonic.local/get/{}/{}".format(x, y),
|
||||||
|
)
|
||||||
|
|
||||||
# verify data is stored
|
# verify data is stored
|
||||||
self.assertDbCountEqual(PodcastChannel, test_range)
|
self.assertDbCountEqual(PodcastChannel, test_range)
|
||||||
@ -147,7 +165,7 @@ class PodcastTestCase(ApiTestBase):
|
|||||||
for x in range(test_range):
|
for x in range(test_range):
|
||||||
channel = channels[x]
|
channel = channels[x]
|
||||||
self.assertTrue(channel.get("url").endswith("podcast-{}".format(x)))
|
self.assertTrue(channel.get("url").endswith("podcast-{}".format(x)))
|
||||||
self.assertTrue(channel.get("status").endswith("new"))
|
self.assertEqual(channel.get("status"), "new")
|
||||||
|
|
||||||
# test for non-admin access
|
# test for non-admin access
|
||||||
rv, channels = self._make_request(
|
rv, channels = self._make_request(
|
||||||
|
330
tests/fixtures/rssfeed.xml
vendored
Normal file
330
tests/fixtures/rssfeed.xml
vendored
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- copied from https://feeds.npr.org/344098539/podcast.xml -->
|
||||||
|
<rss xmlns:npr="https://www.npr.org/rss/" xmlns:nprml="https://api.npr.org/nprml" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
|
||||||
|
<channel>
|
||||||
|
<title>Wait Wait... Don't Tell Me!</title>
|
||||||
|
<link>http://www.npr.org/programs/wait-wait-dont-tell-me/</link>
|
||||||
|
<description><![CDATA[NPR's weekly current events quiz. Have a laugh and test your news knowledge while figuring out what's real and what we've made up.]]></description>
|
||||||
|
<copyright>Copyright 2014-2020 NPR - For Personal Use Only</copyright>
|
||||||
|
<generator>NPR API RSS Generator 0.94</generator>
|
||||||
|
<language>en</language>
|
||||||
|
<itunes:summary><![CDATA[NPR's weekly current events quiz. Have a laugh and test your news knowledge while figuring out what's real and what we've made up.]]></itunes:summary>
|
||||||
|
<itunes:author>NPR</itunes:author>
|
||||||
|
<itunes:block>no</itunes:block>
|
||||||
|
<itunes:owner>
|
||||||
|
<itunes:email>podcasts@npr.org</itunes:email>
|
||||||
|
<itunes:name>NPR</itunes:name>
|
||||||
|
</itunes:owner>
|
||||||
|
<itunes:category text="Comedy"/>
|
||||||
|
<itunes:category text="Leisure">
|
||||||
|
<itunes:category text="Other Games"/>
|
||||||
|
</itunes:category>
|
||||||
|
<itunes:image href="https://media.npr.org/assets/img/2019/11/20/npr_91201389-1-_sq-ca0128ef6da8bbc571cf2bc15e5aecb4d1b33fb4.jpg?s=1400"/>
|
||||||
|
<image>
|
||||||
|
<url>https://media.npr.org/assets/img/2019/11/20/npr_91201389-1-_sq-ca0128ef6da8bbc571cf2bc15e5aecb4d1b33fb4.jpg?s=1400</url>
|
||||||
|
<title>Wait Wait... Don't Tell Me!</title>
|
||||||
|
<link>http://www.npr.org/programs/wait-wait-dont-tell-me/</link>
|
||||||
|
</image>
|
||||||
|
<lastBuildDate>Sat, 08 Aug 2020 12:00:00 -0400</lastBuildDate>
|
||||||
|
<item>
|
||||||
|
<title>Bryan Cranston</title>
|
||||||
|
<description><![CDATA[Bryan Cranston, star of Breaking Bad, joins us along with panelists Tracy Clayton, Joel Kim Booster and Tom Bodett.]]></description>
|
||||||
|
<pubDate>Sat, 08 Aug 2020 12:00:00 -0400</pubDate>
|
||||||
|
<copyright>Copyright 2014-2020 NPR - For Personal Use Only</copyright>
|
||||||
|
<guid isPermalink="false">623b04b0-439a-4c8d-b338-2f0ee6464e07</guid>
|
||||||
|
<itunes:title>Bryan Cranston</itunes:title>
|
||||||
|
<itunes:author>NPR</itunes:author>
|
||||||
|
<itunes:summary><![CDATA[Bryan Cranston, star of Breaking Bad, joins us along with panelists Tracy Clayton, Joel Kim Booster and Tom Bodett.]]></itunes:summary>
|
||||||
|
<itunes:duration>2868</itunes:duration>
|
||||||
|
<itunes:explicit>no</itunes:explicit>
|
||||||
|
<itunes:episodeType>full</itunes:episodeType>
|
||||||
|
<content:encoded><![CDATA[Bryan Cranston, star of Breaking Bad, joins us along with panelists Tracy Clayton, Joel Kim Booster and Tom Bodett.]]></content:encoded>
|
||||||
|
<enclosure url="https://play.podtrac.com/npr-344098539/edge1.pod.npr.org/anon.npr-podcasts/podcast/npr/waitwait/2020/08/20200808_waitwait_wwdtmpodcast200808-5ca1a228-7ed4-41aa-9544-7583a216cb08.mp3?awCollectionId=344098539&awEpisodeId=900413773&orgId=1&d=2868&p=344098539&story=900413773&t=podcast&e=900413773&size=45788644&ft=pod&f=344098539" length="45788644" type="audio/mpeg"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Ramy Youssef</title>
|
||||||
|
<description><![CDATA[Ramy Youssef, actor and comedian, joins us along with panelists Helen Hong, Josh Gondelman, and Negin Farsad.]]></description>
|
||||||
|
<pubDate>Sat, 01 Aug 2020 12:00:00 -0400</pubDate>
|
||||||
|
<copyright>Copyright 2014-2020 NPR - For Personal Use Only</copyright>
|
||||||
|
<guid isPermalink="false">1dab26dc-a02c-4352-8057-0380a2cce384</guid>
|
||||||
|
<itunes:title>Ramy Youssef</itunes:title>
|
||||||
|
<itunes:author>NPR</itunes:author>
|
||||||
|
<itunes:summary><![CDATA[Ramy Youssef, actor and comedian, joins us along with panelists Helen Hong, Josh Gondelman, and Negin Farsad.]]></itunes:summary>
|
||||||
|
<itunes:duration>2959</itunes:duration>
|
||||||
|
<itunes:explicit>no</itunes:explicit>
|
||||||
|
<itunes:episodeType>full</itunes:episodeType>
|
||||||
|
<content:encoded><![CDATA[Ramy Youssef, actor and comedian, joins us along with panelists Helen Hong, Josh Gondelman, and Negin Farsad.]]></content:encoded>
|
||||||
|
<enclosure url="https://play.podtrac.com/npr-344098539/edge1.pod.npr.org/anon.npr-podcasts/podcast/npr/waitwait/2020/08/20200801_waitwait_wwdtmpodcast200801-967e78da-26d2-40b7-b42a-dab3285ffa76.mp3?awCollectionId=344098539&awEpisodeId=898065375&orgId=1&d=2959&p=344098539&story=898065375&t=podcast&e=898065375&size=47245974&ft=pod&f=344098539" length="47245974" type="audio/mpeg"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Padma Lakshmi</title>
|
||||||
|
<description><![CDATA[Padma Lakshmi, cooking show host, joins us along with panelists Jessi Klein, Peter Grosz, and Dulcé Sloan.]]></description>
|
||||||
|
<pubDate>Sat, 25 Jul 2020 12:00:00 -0400</pubDate>
|
||||||
|
<copyright>Copyright 2014-2020 NPR - For Personal Use Only</copyright>
|
||||||
|
<guid isPermalink="false">efd39a54-2b6c-4458-8a28-74e27c199fb5</guid>
|
||||||
|
<itunes:title>Padma Lakshmi</itunes:title>
|
||||||
|
<itunes:author>NPR</itunes:author>
|
||||||
|
<itunes:summary><![CDATA[Padma Lakshmi, cooking show host, joins us along with panelists Jessi Klein, Peter Grosz, and Dulcé Sloan.]]></itunes:summary>
|
||||||
|
<itunes:duration>2971</itunes:duration>
|
||||||
|
<itunes:explicit>no</itunes:explicit>
|
||||||
|
<itunes:episodeType>full</itunes:episodeType>
|
||||||
|
<content:encoded><![CDATA[Padma Lakshmi, cooking show host, joins us along with panelists Jessi Klein, Peter Grosz, and Dulcé Sloan.]]></content:encoded>
|
||||||
|
<enclosure url="https://play.podtrac.com/npr-344098539/edge1.pod.npr.org/anon.npr-podcasts/podcast/npr/waitwait/2020/07/20200725_waitwait_wwdtmpodcast200725-7a90df4d-edef-4eab-b604-6f0255a490c8.mp3?awCollectionId=344098539&awEpisodeId=895317988&orgId=1&d=2971&p=344098539&story=895317988&t=podcast&e=895317988&size=47443215&ft=pod&f=344098539" length="47443215" type="audio/mpeg"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Maria Konnikova</title>
|
||||||
|
<description><![CDATA[Maria Konnikova, author and professional poker player, joins us along with panelists Demi Adejuyigbe, Amy Dickinson, and Alonzo Bodden.]]></description>
|
||||||
|
<pubDate>Sat, 18 Jul 2020 12:00:00 -0400</pubDate>
|
||||||
|
<copyright>Copyright 2014-2020 NPR - For Personal Use Only</copyright>
|
||||||
|
<guid isPermalink="false">578fd11b-0448-406a-a63d-99b536c43318</guid>
|
||||||
|
<itunes:title>Maria Konnikova</itunes:title>
|
||||||
|
<itunes:author>NPR</itunes:author>
|
||||||
|
<itunes:summary><![CDATA[Maria Konnikova, author and professional poker player, joins us along with panelists Demi Adejuyigbe, Amy Dickinson, and Alonzo Bodden.]]></itunes:summary>
|
||||||
|
<itunes:duration>2927</itunes:duration>
|
||||||
|
<itunes:explicit>no</itunes:explicit>
|
||||||
|
<itunes:episodeType>full</itunes:episodeType>
|
||||||
|
<content:encoded><![CDATA[Maria Konnikova, author and professional poker player, joins us along with panelists Demi Adejuyigbe, Amy Dickinson, and Alonzo Bodden.]]></content:encoded>
|
||||||
|
<enclosure url="https://play.podtrac.com/npr-344098539/edge1.pod.npr.org/anon.npr-podcasts/podcast/npr/waitwait/2020/07/20200718_waitwait_wwdtmpodcast200718-907f5a4e-78cd-40d2-9734-e72c836e31a1.mp3?awCollectionId=344098539&awEpisodeId=892516458&orgId=1&d=2927&p=344098539&story=892516458&t=podcast&e=892516458&size=46729728&ft=pod&f=344098539" length="46729728" type="audio/mpeg"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Bonus Podcast: Tur-Bill Tax</title>
|
||||||
|
<description><![CDATA[Do your taxes with Bill Kurtis.]]></description>
|
||||||
|
<pubDate>Tue, 14 Jul 2020 18:55:00 -0400</pubDate>
|
||||||
|
<copyright>Copyright 2014-2020 NPR - For Personal Use Only</copyright>
|
||||||
|
<guid isPermalink="false">9ed4bdef-145b-4e45-809e-e74877842d03</guid>
|
||||||
|
<itunes:title>Bonus Podcast: Tur-Bill Tax</itunes:title>
|
||||||
|
<itunes:author>NPR</itunes:author>
|
||||||
|
<itunes:summary><![CDATA[Do your taxes with Bill Kurtis.]]></itunes:summary>
|
||||||
|
<itunes:duration>74</itunes:duration>
|
||||||
|
<itunes:explicit>no</itunes:explicit>
|
||||||
|
<itunes:episodeType>full</itunes:episodeType>
|
||||||
|
<content:encoded><![CDATA[Do your taxes with Bill Kurtis.]]></content:encoded>
|
||||||
|
<enclosure url="https://play.podtrac.com/npr-344098539/edge1.pod.npr.org/anon.npr-mp3/npr/waitwait/2020/07/20200714_waitwait_wwdtmpodcast200714.mp3?awCollectionId=344098539&awEpisodeId=891207787&orgId=1&d=74&p=344098539&story=891207787&t=podcast&e=891207787&size=1192620&ft=pod&f=344098539" length="1192620" type="audio/mpeg"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Jameela Jamil</title>
|
||||||
|
<description><![CDATA[Jameela Jamil, actor, joins us along with panelists Paula Poundstone, Maz Jobrani, and Maeve Higgins.]]></description>
|
||||||
|
<pubDate>Sat, 11 Jul 2020 12:00:00 -0400</pubDate>
|
||||||
|
<copyright>Copyright 2014-2020 NPR - For Personal Use Only</copyright>
|
||||||
|
<guid isPermalink="false">41868a31-df3e-48e6-b572-2ff0b0e4ae29</guid>
|
||||||
|
<itunes:title>Jameela Jamil</itunes:title>
|
||||||
|
<itunes:author>NPR</itunes:author>
|
||||||
|
<itunes:summary><![CDATA[Jameela Jamil, actor, joins us along with panelists Paula Poundstone, Maz Jobrani, and Maeve Higgins.]]></itunes:summary>
|
||||||
|
<itunes:duration>2872</itunes:duration>
|
||||||
|
<itunes:explicit>no</itunes:explicit>
|
||||||
|
<itunes:episodeType>full</itunes:episodeType>
|
||||||
|
<content:encoded><![CDATA[Jameela Jamil, actor, joins us along with panelists Paula Poundstone, Maz Jobrani, and Maeve Higgins.]]></content:encoded>
|
||||||
|
<enclosure url="https://play.podtrac.com/npr-344098539/edge1.pod.npr.org/anon.npr-podcasts/podcast/npr/waitwait/2020/07/20200711_waitwait_wwdtmpodcast200711-9d1b6517-af16-4d49-a734-0044d0cb0dd9.mp3?awCollectionId=344098539&awEpisodeId=889958701&orgId=1&d=2872&p=344098539&story=889958701&t=podcast&e=889958701&size=45860219&ft=pod&f=344098539" length="45860219" type="audio/mpeg"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>WWDTM Quarantine Edition</title>
|
||||||
|
<description><![CDATA[Tom Hanks, Big Boi, and others join us for this quarantine edition of our show.]]></description>
|
||||||
|
<pubDate>Sat, 04 Jul 2020 12:00:00 -0400</pubDate>
|
||||||
|
<copyright>Copyright 2014-2020 NPR - For Personal Use Only</copyright>
|
||||||
|
<guid isPermalink="false">c9b64706-efc5-4a8b-93ee-1f4323c8bfca</guid>
|
||||||
|
<itunes:title>WWDTM Quarantine Edition</itunes:title>
|
||||||
|
<itunes:author>NPR</itunes:author>
|
||||||
|
<itunes:summary><![CDATA[Tom Hanks, Big Boi, and others join us for this quarantine edition of our show.]]></itunes:summary>
|
||||||
|
<itunes:duration>2881</itunes:duration>
|
||||||
|
<itunes:explicit>no</itunes:explicit>
|
||||||
|
<itunes:episodeType>full</itunes:episodeType>
|
||||||
|
<content:encoded><![CDATA[Tom Hanks, Big Boi, and others join us for this quarantine edition of our show.]]></content:encoded>
|
||||||
|
<enclosure url="https://play.podtrac.com/npr-344098539/edge1.pod.npr.org/anon.npr-podcasts/podcast/npr/waitwait/2020/07/20200704_waitwait_wwdtmpodcast200704-4212d553-533b-404c-b0f4-d8381d3d1c82.mp3?awCollectionId=344098539&awEpisodeId=886443892&orgId=1&d=2881&p=344098539&story=886443892&t=podcast&e=886443892&size=46002897&ft=pod&f=344098539" length="46002897" type="audio/mpeg"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Don Cheadle</title>
|
||||||
|
<description><![CDATA[Don Cheadle, actor, joins us along with panelists Mo Rocca, Faith Salie, and Hari Kondabolu.]]></description>
|
||||||
|
<pubDate>Sat, 27 Jun 2020 12:00:00 -0400</pubDate>
|
||||||
|
<copyright>Copyright 2014-2020 NPR - For Personal Use Only</copyright>
|
||||||
|
<guid isPermalink="false">4662e484-dac3-4692-8e7a-e2b5ff176979</guid>
|
||||||
|
<itunes:title>Don Cheadle</itunes:title>
|
||||||
|
<itunes:author>NPR</itunes:author>
|
||||||
|
<itunes:summary><![CDATA[Don Cheadle, actor, joins us along with panelists Mo Rocca, Faith Salie, and Hari Kondabolu.]]></itunes:summary>
|
||||||
|
<itunes:duration>2990</itunes:duration>
|
||||||
|
<itunes:explicit>no</itunes:explicit>
|
||||||
|
<itunes:episodeType>full</itunes:episodeType>
|
||||||
|
<content:encoded><![CDATA[Don Cheadle, actor, joins us along with panelists Mo Rocca, Faith Salie, and Hari Kondabolu.]]></content:encoded>
|
||||||
|
<enclosure url="https://play.podtrac.com/npr-344098539/edge1.pod.npr.org/anon.npr-mp3/npr/waitwait/2020/06/20200627_waitwait_wwdtmpodcast200627.mp3?awCollectionId=344098539&awEpisodeId=882852605&orgId=1&d=2990&p=344098539&story=882852605&t=podcast&e=882852605&size=47731071&ft=pod&f=344098539" length="47731071" type="audio/mpeg"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Dan Riskin</title>
|
||||||
|
<description><![CDATA[Dan Riskin, bat expert, joins us along with panelists Tom Papa, Roxanne Roberts, and Joel Kim Booster.]]></description>
|
||||||
|
<pubDate>Sat, 20 Jun 2020 12:00:00 -0400</pubDate>
|
||||||
|
<copyright>Copyright 2014-2020 NPR - For Personal Use Only</copyright>
|
||||||
|
<guid isPermalink="false">bc6029ab-2bec-4fe5-aa4f-d570a29257b7</guid>
|
||||||
|
<itunes:title>Dan Riskin</itunes:title>
|
||||||
|
<itunes:author>NPR</itunes:author>
|
||||||
|
<itunes:summary><![CDATA[Dan Riskin, bat expert, joins us along with panelists Tom Papa, Roxanne Roberts, and Joel Kim Booster.]]></itunes:summary>
|
||||||
|
<itunes:duration>2933</itunes:duration>
|
||||||
|
<itunes:explicit>no</itunes:explicit>
|
||||||
|
<itunes:episodeType>full</itunes:episodeType>
|
||||||
|
<content:encoded><![CDATA[Dan Riskin, bat expert, joins us along with panelists Tom Papa, Roxanne Roberts, and Joel Kim Booster.]]></content:encoded>
|
||||||
|
<enclosure url="https://play.podtrac.com/npr-344098539/edge1.pod.npr.org/anon.npr-podcasts/podcast/npr/waitwait/2020/06/20200620_waitwait_wwdtmpodcast200620-0b3082ff-f961-4f87-8269-c9db3a5f5242.mp3?awCollectionId=344098539&awEpisodeId=881152894&orgId=1&d=2933&p=344098539&story=881152894&t=podcast&e=881152894&size=46823553&ft=pod&f=344098539" length="46823553" type="audio/mpeg"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Ashima Shiraishi</title>
|
||||||
|
<description><![CDATA[Ashima Shiraishi, mountain climber, joins us along with panelists Jessi Klein, Josh Gondelman, and Negin Farsad.]]></description>
|
||||||
|
<pubDate>Sat, 13 Jun 2020 12:00:00 -0400</pubDate>
|
||||||
|
<copyright>Copyright 2014-2020 NPR - For Personal Use Only</copyright>
|
||||||
|
<guid isPermalink="false">98aaa454-fd63-4edc-8236-c2499f37a5fc</guid>
|
||||||
|
<itunes:title>Ashima Shiraishi</itunes:title>
|
||||||
|
<itunes:author>NPR</itunes:author>
|
||||||
|
<itunes:summary><![CDATA[Ashima Shiraishi, mountain climber, joins us along with panelists Jessi Klein, Josh Gondelman, and Negin Farsad.]]></itunes:summary>
|
||||||
|
<itunes:duration>2960</itunes:duration>
|
||||||
|
<itunes:explicit>no</itunes:explicit>
|
||||||
|
<itunes:episodeType>full</itunes:episodeType>
|
||||||
|
<content:encoded><![CDATA[Ashima Shiraishi, mountain climber, joins us along with panelists Jessi Klein, Josh Gondelman, and Negin Farsad.]]></content:encoded>
|
||||||
|
<enclosure url="https://play.podtrac.com/npr-344098539/edge1.pod.npr.org/anon.npr-podcasts/podcast/npr/waitwait/2020/06/20200613_waitwait_wwdtmpodcast200613-bf47bf29-cd3f-4cde-8083-574fde99bd5c.mp3?awCollectionId=344098539&awEpisodeId=876485379&orgId=1&d=2960&p=344098539&story=876485379&t=podcast&e=876485379&size=47261318&ft=pod&f=344098539" length="47261318" type="audio/mpeg"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Sarah Cooper</title>
|
||||||
|
<description><![CDATA[Sarah Cooper, comedian, joins us along with panelists Alonzo Bodden, Helen Hong, and Peter Grosz.]]></description>
|
||||||
|
<pubDate>Sat, 06 Jun 2020 12:00:00 -0400</pubDate>
|
||||||
|
<copyright>Copyright 2014-2020 NPR - For Personal Use Only</copyright>
|
||||||
|
<guid isPermalink="false">db687322-1327-4ad2-a1cb-251a988da5fd</guid>
|
||||||
|
<itunes:title>Sarah Cooper</itunes:title>
|
||||||
|
<itunes:author>NPR</itunes:author>
|
||||||
|
<itunes:summary><![CDATA[Sarah Cooper, comedian, joins us along with panelists Alonzo Bodden, Helen Hong, and Peter Grosz.]]></itunes:summary>
|
||||||
|
<itunes:duration>2926</itunes:duration>
|
||||||
|
<itunes:explicit>no</itunes:explicit>
|
||||||
|
<itunes:episodeType>full</itunes:episodeType>
|
||||||
|
<content:encoded><![CDATA[Sarah Cooper, comedian, joins us along with panelists Alonzo Bodden, Helen Hong, and Peter Grosz.]]></content:encoded>
|
||||||
|
<enclosure url="https://play.podtrac.com/npr-344098539/edge1.pod.npr.org/anon.npr-podcasts/podcast/npr/waitwait/2020/06/20200606_waitwait_wwdtmpodcast200606-9c2df705-08c0-4e11-a236-f71880d03107.mp3?awCollectionId=344098539&awEpisodeId=871377580&orgId=1&d=2926&p=344098539&story=871377580&t=podcast&e=871377580&size=46723056&ft=pod&f=344098539" length="46723056" type="audio/mpeg"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Tony Hawk and Jeff Tweedy</title>
|
||||||
|
<description><![CDATA[This week we present an around-the-country of WWDTM, along with special guests Tony Hawk and Jeff Tweedy.]]></description>
|
||||||
|
<pubDate>Sat, 30 May 2020 12:00:00 -0400</pubDate>
|
||||||
|
<copyright>Copyright 2014-2020 NPR - For Personal Use Only</copyright>
|
||||||
|
<guid isPermalink="false">0e0c5ee0-3733-45bc-979f-20b627a95c09</guid>
|
||||||
|
<itunes:title>Tony Hawk and Jeff Tweedy</itunes:title>
|
||||||
|
<itunes:author>NPR</itunes:author>
|
||||||
|
<itunes:summary><![CDATA[This week we present an around-the-country of WWDTM, along with special guests Tony Hawk and Jeff Tweedy.]]></itunes:summary>
|
||||||
|
<itunes:duration>2907</itunes:duration>
|
||||||
|
<itunes:explicit>no</itunes:explicit>
|
||||||
|
<itunes:episodeType>full</itunes:episodeType>
|
||||||
|
<content:encoded><![CDATA[This week we present an around-the-country of WWDTM, along with special guests Tony Hawk and Jeff Tweedy.]]></content:encoded>
|
||||||
|
<enclosure url="https://play.podtrac.com/npr-344098539/edge1.pod.npr.org/anon.npr-podcasts/podcast/npr/waitwait/2020/05/20200530_waitwait_wwdtmpodcast200530-6042596f-402c-4455-9bcb-ddc19fad353b.mp3?awCollectionId=344098539&awEpisodeId=859546414&orgId=1&d=2907&p=344098539&story=859546414&t=podcast&e=859546414&size=46409857&ft=pod&f=344098539" length="46409857" type="audio/mpeg"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Christina Koch</title>
|
||||||
|
<description><![CDATA[Christina Koch, NASA engineer and astronaut, joins us along with panelists Tom Bodett, Alison Leiby, and Maz Jobrani.]]></description>
|
||||||
|
<pubDate>Sat, 23 May 2020 12:00:00 -0400</pubDate>
|
||||||
|
<copyright>Copyright 2014-2020 NPR - For Personal Use Only</copyright>
|
||||||
|
<guid isPermalink="false">b13d7726-d82a-4e00-b23c-d09e3e0d383e</guid>
|
||||||
|
<itunes:title>Christina Koch</itunes:title>
|
||||||
|
<itunes:author>NPR</itunes:author>
|
||||||
|
<itunes:summary><![CDATA[Christina Koch, NASA engineer and astronaut, joins us along with panelists Tom Bodett, Alison Leiby, and Maz Jobrani.]]></itunes:summary>
|
||||||
|
<itunes:duration>2954</itunes:duration>
|
||||||
|
<itunes:explicit>no</itunes:explicit>
|
||||||
|
<itunes:episodeType>full</itunes:episodeType>
|
||||||
|
<content:encoded><![CDATA[Christina Koch, NASA engineer and astronaut, joins us along with panelists Tom Bodett, Alison Leiby, and Maz Jobrani.]]></content:encoded>
|
||||||
|
<enclosure url="https://play.podtrac.com/npr-344098539/edge1.pod.npr.org/anon.npr-podcasts/podcast/npr/waitwait/2020/05/20200526_waitwait_wwdtmpodcast200523-178735eb-75ee-4422-bcf6-63b7156c04e9.mp3?awCollectionId=344098539&awEpisodeId=859541911&orgId=1&d=2954&p=344098539&story=859541911&t=podcast&e=859541911&size=47156651&ft=pod&f=344098539" length="47156651" type="audio/mpeg"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Adam Rippon</title>
|
||||||
|
<description><![CDATA[Adam Rippon, American former figure skater, joins us along with panelists Paula Poundstone, Tom Papa, and Negin Farsad.]]></description>
|
||||||
|
<pubDate>Sat, 16 May 2020 12:00:00 -0400</pubDate>
|
||||||
|
<copyright>Copyright 2014-2020 NPR - For Personal Use Only</copyright>
|
||||||
|
<guid isPermalink="false">709d41f4-e5be-45ed-83fe-543282c2051d</guid>
|
||||||
|
<itunes:title>Adam Rippon</itunes:title>
|
||||||
|
<itunes:author>NPR</itunes:author>
|
||||||
|
<itunes:summary><![CDATA[Adam Rippon, American former figure skater, joins us along with panelists Paula Poundstone, Tom Papa, and Negin Farsad.]]></itunes:summary>
|
||||||
|
<itunes:duration>2947</itunes:duration>
|
||||||
|
<itunes:explicit>no</itunes:explicit>
|
||||||
|
<itunes:episodeType>full</itunes:episodeType>
|
||||||
|
<content:encoded><![CDATA[Adam Rippon, American former figure skater, joins us along with panelists Paula Poundstone, Tom Papa, and Negin Farsad.]]></content:encoded>
|
||||||
|
<enclosure url="https://play.podtrac.com/npr-344098539/edge1.pod.npr.org/anon.npr-podcasts/podcast/npr/waitwait/2020/05/20200516_waitwait_wwdtmpodcast200516-0d01c195-fb05-45ee-8192-a1cb031034ab.mp3?awCollectionId=344098539&awEpisodeId=855431671&orgId=1&d=2947&p=344098539&story=855431671&t=podcast&e=855431671&size=47052069&ft=pod&f=344098539" length="47052069" type="audio/mpeg"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Bonus Podcast: Bill Kurtis In the Wild</title>
|
||||||
|
<description><![CDATA[Go on a nature walk with Bill Kurtis.]]></description>
|
||||||
|
<pubDate>Tue, 12 May 2020 18:50:00 -0400</pubDate>
|
||||||
|
<copyright>Copyright 2014-2020 NPR - For Personal Use Only</copyright>
|
||||||
|
<guid isPermalink="false">3903c8b4-b735-45c6-bf09-2b237b6d5e15</guid>
|
||||||
|
<itunes:title>Bonus Podcast: Bill Kurtis In the Wild</itunes:title>
|
||||||
|
<itunes:author>NPR</itunes:author>
|
||||||
|
<itunes:summary><![CDATA[Go on a nature walk with Bill Kurtis.]]></itunes:summary>
|
||||||
|
<itunes:duration>139</itunes:duration>
|
||||||
|
<itunes:explicit>no</itunes:explicit>
|
||||||
|
<itunes:episodeType>full</itunes:episodeType>
|
||||||
|
<content:encoded><![CDATA[Go on a nature walk with Bill Kurtis.]]></content:encoded>
|
||||||
|
<enclosure url="https://play.podtrac.com/npr-344098539/edge1.pod.npr.org/anon.npr-mp3/npr/waitwait/2020/05/20200512_waitwait_wwdtmpodcast200512.mp3?awCollectionId=344098539&awEpisodeId=854992463&orgId=1&d=139&p=344098539&story=854992463&t=podcast&e=854992463&size=2233869&ft=pod&f=344098539" length="2233869" type="audio/mpeg"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Samantha Bee</title>
|
||||||
|
<description><![CDATA[Samantha Bee, host of Full Frontal, joins us along with panelists Mo Rocca, Helen Hong, and Adam Burke.]]></description>
|
||||||
|
<pubDate>Sat, 09 May 2020 12:00:00 -0400</pubDate>
|
||||||
|
<copyright>Copyright 2014-2020 NPR - For Personal Use Only</copyright>
|
||||||
|
<guid>29550aa1-e223-4496-b394-bdd685b0c08f</guid>
|
||||||
|
<itunes:title>Samantha Bee</itunes:title>
|
||||||
|
<itunes:author>NPR</itunes:author>
|
||||||
|
<itunes:summary><![CDATA[Samantha Bee, host of Full Frontal, joins us along with panelists Mo Rocca, Helen Hong, and Adam Burke.]]></itunes:summary>
|
||||||
|
<itunes:duration>2926</itunes:duration>
|
||||||
|
<itunes:explicit>no</itunes:explicit>
|
||||||
|
<itunes:episodeType>full</itunes:episodeType>
|
||||||
|
<content:encoded><![CDATA[Samantha Bee, host of Full Frontal, joins us along with panelists Mo Rocca, Helen Hong, and Adam Burke.]]></content:encoded>
|
||||||
|
<enclosure url="https://play.podtrac.com/npr-344098539/edge1.pod.npr.org/anon.npr-podcasts/podcast/npr/waitwait/2020/05/20200509_waitwait_wwdtmpodcast200509-61fdca60-cedf-49e1-84da-af365de04284-8b96f3a6-d3a7-4012-a869-6d52fd2c7859-56ddff14-4343-430b-91ad-011f71f0fce9.mp3?awCollectionId=344098539&awEpisodeId=851494348&orgId=1&d=2926&p=344098539&story=851494348&t=podcast&e=851494348&size=46720105&ft=pod&f=344098539" length="46720105" type="audio/mpeg"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Christine Baranski</title>
|
||||||
|
<description><![CDATA[Christine Baranski, actor, joins us along with panelists Alonzo Bodden, Amy Dickenson, and Joel Kim Booster.]]></description>
|
||||||
|
<pubDate>Sat, 02 May 2020 12:00:00 -0400</pubDate>
|
||||||
|
<copyright>Copyright 2014-2020 NPR - For Personal Use Only</copyright>
|
||||||
|
<guid>1c9b2452-02c9-4897-9f32-01156fc0bf8a</guid>
|
||||||
|
<itunes:title>Christine Baranski</itunes:title>
|
||||||
|
<itunes:author>NPR</itunes:author>
|
||||||
|
<itunes:summary><![CDATA[Christine Baranski, actor, joins us along with panelists Alonzo Bodden, Amy Dickenson, and Joel Kim Booster.]]></itunes:summary>
|
||||||
|
<itunes:duration>2995</itunes:duration>
|
||||||
|
<itunes:explicit>no</itunes:explicit>
|
||||||
|
<itunes:episodeType>full</itunes:episodeType>
|
||||||
|
<content:encoded><![CDATA[Christine Baranski, actor, joins us along with panelists Alonzo Bodden, Amy Dickenson, and Joel Kim Booster.]]></content:encoded>
|
||||||
|
<enclosure url="https://play.podtrac.com/npr-344098539/edge1.pod.npr.org/anon.npr-podcasts/podcast/npr/waitwait/2020/05/20200502_waitwait_wwdtmpodcast200502-db024d89-d0de-433a-aa90-c6d2f58fa35b-c3642499-2b18-4d61-a11a-318e24223633.mp3?awCollectionId=344098539&awEpisodeId=847820014&orgId=1&d=2995&p=344098539&story=847820014&t=podcast&e=847820014&size=47815094&ft=pod&f=344098539" length="47815094" type="audio/mpeg"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Allison Janney</title>
|
||||||
|
<description><![CDATA[Allison Janney, actor, joins us along with panelists Mo Rocca, Faith Salie, and Luke Burbank.]]></description>
|
||||||
|
<pubDate>Sat, 25 Apr 2020 12:00:00 -0400</pubDate>
|
||||||
|
<copyright>Copyright 2014-2020 NPR - For Personal Use Only</copyright>
|
||||||
|
<guid>01528da1-fa6f-4177-a19d-9aafb1926a86</guid>
|
||||||
|
<itunes:title>Allison Janney</itunes:title>
|
||||||
|
<itunes:author>NPR</itunes:author>
|
||||||
|
<itunes:summary><![CDATA[Allison Janney, actor, joins us along with panelists Mo Rocca, Faith Salie, and Luke Burbank.]]></itunes:summary>
|
||||||
|
<itunes:duration>2992</itunes:duration>
|
||||||
|
<itunes:explicit>no</itunes:explicit>
|
||||||
|
<itunes:episodeType>full</itunes:episodeType>
|
||||||
|
<content:encoded><![CDATA[Allison Janney, actor, joins us along with panelists Mo Rocca, Faith Salie, and Luke Burbank.]]></content:encoded>
|
||||||
|
<enclosure url="https://play.podtrac.com/npr-344098539/edge1.pod.npr.org/anon.npr-podcasts/podcast/npr/waitwait/2020/04/20200425_waitwait_wwdtmpodcast200425-7a06ad0c-3379-46bb-91f8-b276ca8fe6c5.mp3?awCollectionId=344098539&awEpisodeId=842007544&orgId=1&d=2992&p=344098539&story=842007544&t=podcast&e=842007544&size=47763471&ft=pod&f=344098539" length="47763471" type="audio/mpeg"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Tom Hanks At Home</title>
|
||||||
|
<description><![CDATA[Tom Hanks, sometime public radio host, joins us along with panelists Adam Felber, Negin Farsad, and Peter Grosz.]]></description>
|
||||||
|
<pubDate>Sat, 18 Apr 2020 12:00:00 -0400</pubDate>
|
||||||
|
<copyright>Copyright 2014-2020 NPR - For Personal Use Only</copyright>
|
||||||
|
<guid>61215c56-640e-4fc0-bef3-d88810aa48b6</guid>
|
||||||
|
<itunes:title>Tom Hanks At Home</itunes:title>
|
||||||
|
<itunes:author>NPR</itunes:author>
|
||||||
|
<itunes:summary><![CDATA[Tom Hanks, sometime public radio host, joins us along with panelists Adam Felber, Negin Farsad, and Peter Grosz.]]></itunes:summary>
|
||||||
|
<itunes:duration>2994</itunes:duration>
|
||||||
|
<itunes:explicit>no</itunes:explicit>
|
||||||
|
<itunes:episodeType>full</itunes:episodeType>
|
||||||
|
<content:encoded><![CDATA[Tom Hanks, sometime public radio host, joins us along with panelists Adam Felber, Negin Farsad, and Peter Grosz.]]></content:encoded>
|
||||||
|
<enclosure url="https://play.podtrac.com/npr-344098539/edge1.pod.npr.org/anon.npr-podcasts/podcast/npr/waitwait/2020/04/20200418_waitwait_wwdtmpodcast200418-a61351d5-80c3-46af-9fd9-c307fa36ebcd.mp3?awCollectionId=344098539&awEpisodeId=837911871&orgId=1&d=2994&p=344098539&story=837911871&t=podcast&e=837911871&size=47801086&ft=pod&f=344098539" length="47801086" type="audio/mpeg"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<title>Samin Nosrat</title>
|
||||||
|
<description><![CDATA[Samin Nosrat, chef, joins us along with panelists Tom Bodett, Helen Hong, and Josh Gondelman.]]></description>
|
||||||
|
<pubDate>Sat, 11 Apr 2020 12:00:00 -0400</pubDate>
|
||||||
|
<copyright>Copyright 2014-2020 NPR - For Personal Use Only</copyright>
|
||||||
|
<guid>be3fe3c6-153b-4f9a-8082-56eedf498adc</guid>
|
||||||
|
<itunes:title>Samin Nosrat</itunes:title>
|
||||||
|
<itunes:author>NPR</itunes:author>
|
||||||
|
<itunes:summary><![CDATA[Samin Nosrat, chef, joins us along with panelists Tom Bodett, Helen Hong, and Josh Gondelman.]]></itunes:summary>
|
||||||
|
<itunes:duration>2933</itunes:duration>
|
||||||
|
<itunes:explicit>no</itunes:explicit>
|
||||||
|
<itunes:episodeType>full</itunes:episodeType>
|
||||||
|
<content:encoded><![CDATA[Samin Nosrat, chef, joins us along with panelists Tom Bodett, Helen Hong, and Josh Gondelman.]]></content:encoded>
|
||||||
|
<enclosure url="https://play.podtrac.com/npr-344098539/edge1.pod.npr.org/anon.npr-podcasts/podcast/npr/waitwait/2020/04/20200411_waitwait_wwdtmpodcast200411-78ba83c4-b87e-423f-a77c-8e97a944746b.mp3?awCollectionId=344098539&awEpisodeId=832354659&orgId=1&d=2933&p=344098539&story=832354659&t=podcast&e=832354659&size=46828225&ft=pod&f=344098539" length="46828225" type="audio/mpeg"/>
|
||||||
|
</item>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
Loading…
Reference in New Issue
Block a user