2020-09-21 Fred Gleason <fredg@paravelsystems.com>

* Added a 'PODCASTS.SHA1_HASH' field to the database.
	* Incremented the database version to 335.
	* Added 'RDPodcast::sha1Hash()' and 'RDPodcast::setSha1Hash()'
	methods.
	* Implemented audio relinking for podcast media files.

Signed-off-by: Fred Gleason <fredg@paravelsystems.com>
This commit is contained in:
Fred Gleason 2020-09-21 10:24:00 -04:00
parent 6516c20ff6
commit 6d3a60d174
13 changed files with 387 additions and 62 deletions

View File

@ -20285,3 +20285,9 @@
* Added a 'SavePodcast' method to the Web API.
* Added a 'GetPodcast' method to the Web API.
* Added a 'DeletePodcast' method to the Web API.
2020-09-21 Fred Gleason <fredg@paravelsystems.com>
* Added a 'PODCASTS.SHA1_HASH' field to the database.
* Incremented the database version to 335.
* Added 'RDPodcast::sha1Hash()' and 'RDPodcast::setSha1Hash()'
methods.
* Implemented audio relinking for podcast media files.

View File

@ -20,6 +20,7 @@ ITEM_IMAGE_ID int(11) From FEED_IMAGES.ID
AUDIO_FILENAME varchar(191)
AUDIO_LENGTH int(10) unsigned
AUDIO_TIME int(10) unsigned
SHA1_HASH varchar(40)
ORIGIN_LOGIN_NAME varchar(191) From USERS.LOGIN_NAME
ORIGIN_STATION varchar(64) From STATIONS.NAME
ORIGIN_DATETIME datetime

View File

@ -24,7 +24,7 @@
/*
* Current Database Version
*/
#define RD_VERSION_DATABASE 334
#define RD_VERSION_DATABASE 335
#endif // DBVERSION_H

View File

@ -46,6 +46,7 @@
#include "rdtempdirectory.h"
#include "rdupload.h"
#include "rdwavefile.h"
#include "rdxport_interface.h"
size_t __RDFeed_Readfunction_Callback(char *buffer,size_t size,size_t nitems,
void *userdata)
@ -1093,6 +1094,11 @@ unsigned RDFeed::postCut(const QString &cutname,Error *err)
return 0;
}
emit postProgressChanged(3);
//
// Save to Audio Store
//
SavePodcast(cast_id,tmpfile);
unlink(tmpfile);
delete upload;
@ -1225,6 +1231,10 @@ unsigned RDFeed::postFile(const QString &srcfile,Error *err)
}
delete upload;
//
// Save to Audio Store
//
SavePodcast(cast_id,tmpfile);
unlink(QString(tmpfile)+".wav");
unlink(tmpfile);
@ -1349,6 +1359,10 @@ unsigned RDFeed::postLog(const QString &logname,const QTime &start_time,
emit postProgressChanged(2+log_event->size());
//
// Save to Audio Store
//
SavePodcast(cast_id,tmpfile);
unlink(tmpfile);
//
@ -1673,6 +1687,80 @@ void RDFeed::renderLineStartedData(int lineno,int total_lines)
}
bool RDFeed::SavePodcast(unsigned cast_id,const QString &src_filename) const
{
long response_code;
CURL *curl=NULL;
CURLcode curl_err;
struct curl_httppost *first=NULL;
struct curl_httppost *last=NULL;
//
// Generate POST Data
//
// We have to use multipart here because we have a file to send.
//
curl_formadd(&first,&last,CURLFORM_PTRNAME,"COMMAND",
CURLFORM_COPYCONTENTS,
(const char *)QString().sprintf("%u",RDXPORT_COMMAND_SAVE_PODCAST),
CURLFORM_END);
curl_formadd(&first,&last,CURLFORM_PTRNAME,"LOGIN_NAME",
CURLFORM_COPYCONTENTS,rda->user()->name().toUtf8().constData(),
CURLFORM_END);
curl_formadd(&first,&last,CURLFORM_PTRNAME,"PASSWORD",
CURLFORM_COPYCONTENTS,
rda->user()->password().toUtf8().constData(),CURLFORM_END);
curl_formadd(&first,&last,CURLFORM_PTRNAME,"ID",
CURLFORM_COPYCONTENTS,
(const char *)QString().sprintf("%u",cast_id),
CURLFORM_END);
curl_formadd(&first,&last,CURLFORM_PTRNAME,"FILENAME",
CURLFORM_FILE,src_filename.toUtf8().constData(),
CURLFORM_END);
//
// Set up the transfer
//
if((curl=curl_easy_init())==NULL) {
curl_formfree(first);
return false;
}
curl_easy_setopt(curl,CURLOPT_WRITEDATA,stdout);
curl_easy_setopt(curl,CURLOPT_HTTPPOST,first);
curl_easy_setopt(curl,CURLOPT_USERAGENT,
(const char *)rda->config()->userAgent());
curl_easy_setopt(curl,CURLOPT_TIMEOUT,RD_CURL_TIMEOUT);
curl_easy_setopt(curl,CURLOPT_NOPROGRESS,1);
curl_easy_setopt(curl,CURLOPT_URL,
rda->station()->webServiceUrl(rda->config()).toUtf8().constData());
//
// Send it
//
if((curl_err=curl_easy_perform(curl))!=CURLE_OK) {
curl_easy_cleanup(curl);
curl_formfree(first);
return false;
}
//
// Clean up
//
curl_easy_getinfo(curl,CURLINFO_RESPONSE_CODE,&response_code);
curl_easy_cleanup(curl);
curl_formfree(first);
//
// Process the results
//
if((response_code<200)||(response_code>299)) {
return false;
}
return true;
}
unsigned RDFeed::CreateCast(QString *filename,int bytes,int msecs) const
{
QString sql;

View File

@ -160,6 +160,7 @@ class RDFeed : public QObject
void renderLineStartedData(int lineno,int total_lines);
private:
bool SavePodcast(unsigned cast_id,const QString &src_filename) const;
unsigned CreateCast(QString *filename,int bytes,int msecs) const;
QString ResolveChannelWildcards(const QString &tmplt,RDSqlQuery *chan_q,
const QDateTime &build_datetime);

View File

@ -31,6 +31,7 @@
#include "rdescape_string.h"
#include "rdpodcast.h"
#include "rdurl.h"
#include "rdxport_interface.h"
//
// CURL Callbacks
@ -317,6 +318,18 @@ int RDPodcast::audioTime() const
}
QString RDPodcast::sha1Hash() const
{
return RDGetSqlValue("PODCASTS","ID",podcast_id,"SHA1_HASH").toString();
}
void RDPodcast::setSha1Hash(const QString &str) const
{
SetRow("SHA1_HASH",str);
}
void RDPodcast::setAudioTime(int msecs) const
{
SetRow("AUDIO_TIME",msecs);
@ -367,6 +380,11 @@ bool RDPodcast::removeAudio(RDFeed *feed,QString *err_text,bool log_debug) const
*err_text=RDDelete::errorText(conv_err);
delete conv;
//
// Delete from Audio Store
//
DeletePodcast(id());
return conv_err==RDDelete::ErrorOk;
}
@ -385,29 +403,101 @@ QString RDPodcast::guid(const QString &full_url,unsigned feed_id,
}
bool RDPodcast::DeletePodcast(unsigned cast_id) const
{
long response_code;
CURL *curl=NULL;
CURLcode curl_err;
struct curl_httppost *first=NULL;
struct curl_httppost *last=NULL;
//
// Generate POST Data
//
curl_formadd(&first,&last,CURLFORM_PTRNAME,"COMMAND",
CURLFORM_COPYCONTENTS,
(const char *)QString().sprintf("%u",RDXPORT_COMMAND_DELETE_PODCAST),
CURLFORM_END);
curl_formadd(&first,&last,CURLFORM_PTRNAME,"LOGIN_NAME",
CURLFORM_COPYCONTENTS,rda->user()->name().toUtf8().constData(),
CURLFORM_END);
curl_formadd(&first,&last,CURLFORM_PTRNAME,"PASSWORD",
CURLFORM_COPYCONTENTS,
rda->user()->password().toUtf8().constData(),CURLFORM_END);
curl_formadd(&first,&last,CURLFORM_PTRNAME,"ID",
CURLFORM_COPYCONTENTS,
(const char *)QString().sprintf("%u",cast_id),
CURLFORM_END);
//
// Set up the transfer
//
if((curl=curl_easy_init())==NULL) {
curl_formfree(first);
return false;
}
curl_easy_setopt(curl,CURLOPT_WRITEDATA,stdout);
curl_easy_setopt(curl,CURLOPT_HTTPPOST,first);
curl_easy_setopt(curl,CURLOPT_USERAGENT,
(const char *)rda->config()->userAgent());
curl_easy_setopt(curl,CURLOPT_TIMEOUT,RD_CURL_TIMEOUT);
curl_easy_setopt(curl,CURLOPT_NOPROGRESS,1);
curl_easy_setopt(curl,CURLOPT_URL,
rda->station()->webServiceUrl(rda->config()).toUtf8().constData());
//
// Send it
//
if((curl_err=curl_easy_perform(curl))!=CURLE_OK) {
curl_easy_cleanup(curl);
curl_formfree(first);
return false;
}
//
// Clean up
//
curl_easy_getinfo(curl,CURLINFO_RESPONSE_CODE,&response_code);
curl_easy_cleanup(curl);
curl_formfree(first);
//
// Process the results
//
if((response_code<200)||(response_code>299)) {
return false;
}
return true;
}
void RDPodcast::SetRow(const QString &param,int value) const
{
RDSqlQuery *q;
QString sql;
sql=QString("update PODCASTS set ")+
param+QString().sprintf("=%d where ",value)+
QString().sprintf("ID=%u",podcast_id);
q=new RDSqlQuery(sql);
delete q;
RDSqlQuery::apply(sql);
}
void RDPodcast::SetRow(const QString &param,const QString &value) const
{
RDSqlQuery *q;
QString sql;
sql=QString("update PODCASTS set ")+
param+"=\""+RDEscapeString(value)+"\" where "+
QString().sprintf("ID=%u",podcast_id);
q=new RDSqlQuery(sql);
delete q;
if(value.isNull()) {
sql=QString("update PODCASTS set ")+
param+"=NULL where "+
QString().sprintf("ID=%u",podcast_id);
}
else {
sql=QString("update PODCASTS set ")+
param+"=\""+RDEscapeString(value)+"\" where "+
QString().sprintf("ID=%u",podcast_id);
}
RDSqlQuery::apply(sql);
}

View File

@ -68,6 +68,8 @@ class RDPodcast
void setAudioLength(int len) const;
int audioTime() const;
void setAudioTime(int msecs) const;
QString sha1Hash() const;
void setSha1Hash(const QString &str=QString()) const;
QDateTime expirationDateTime() const;
void setExpirationDateTime(const QDateTime &dt) const;
RDPodcast::Status status() const;
@ -79,6 +81,7 @@ class RDPodcast
unsigned feed_id,unsigned cast_id);
private:
bool DeletePodcast(unsigned cast_id) const;
void SetRow(const QString &param,int value) const;
void SetRow(const QString &param,const QString &value) const;
void SetRow(const QString &param,const QDateTime &datetime,const QString &value) const;

View File

@ -389,60 +389,42 @@ void MainObject::RelinkAudio(const QString &srcdir) const
QString hash=RDSha1Hash(filename);
QString firstdest;
bool delete_source=true;
//
// Check against audio cuts
//
sql=QString("select CUTS.CUT_NAME,CART.TITLE from ")+
"CUTS left join CART "+
"on CUTS.CART_NUMBER=CART.NUMBER where "+
"CUTS.SHA1_HASH=\""+RDEscapeString(hash)+"\"";
q=new RDSqlQuery(sql);
while(q->next()) {
printf(" Recovering %06u/%03d [%s]...",
RDCut::cartNumber(q->value(0).toString()),
RDCut::cutNumber(q->value(0).toString()),
(const char *)q->value(1).toString());
fflush(stdout);
if(db_relink_audio_move) {
unlink(RDCut::pathName(q->value(0).toString()));
if(link(filename,RDCut::pathName(q->value(0).toString()))<0) {
if(errno==EXDEV) {
if(firstdest.isEmpty()) {
if(CopyFile(RDCut::pathName(q->value(0).toString()),filename)) {
firstdest=RDCut::pathName(q->value(0).toString());
}
else {
fprintf(stderr,"unable to copy file \"%s\"\n",
(const char *)filename);
delete_source=false;
}
}
else {
unlink(RDCut::pathName(q->value(0).toString()));
link(firstdest,RDCut::pathName(q->value(0).toString()));
}
}
else {
fprintf(stderr,"unable to move file \"%s\" [%s]\n",
(const char *)filename,strerror(errno));
delete_source=false;
}
}
}
else {
if(firstdest.isEmpty()) {
if(CopyFile(RDCut::pathName(q->value(0).toString()),filename)) {
firstdest=RDCut::pathName(q->value(0).toString());
}
else {
fprintf(stderr,"unable to copy file \"%s\"\n",
(const char *)filename);
}
}
else {
unlink(RDCut::pathName(q->value(0).toString()));
link(firstdest,RDCut::pathName(q->value(0).toString()));
}
}
printf(" done.\n");
RelinkCut(filename,q->value(0).toString(),q->value(1).toString(),
&firstdest,&delete_source);
}
//
// Check against RSS posts
//
sql=QString("select ")+
"FEEDS.KEY_NAME,"+ // 00
"PODCASTS.ID,"+ // 01
"PODCASTS.ITEM_TITLE,"+ // 02
"PODCASTS.AUDIO_FILENAME "+ // 03
"from PODCASTS left join FEEDS "+
"on FEEDS.ID=PODCASTS.FEED_ID where "+
"PODCASTS.SHA1_HASH=\""+RDEscapeString(hash)+"\"";
q=new RDSqlQuery(sql);
while(q->next()) {
RelinkCast(filename,q->value(0).toString(),q->value(1).toUInt(),
q->value(2).toString(),q->value(3).toString(),
&firstdest,&delete_source);
}
delete q;
//
// (Perhaps) delete the source file
//
if(db_relink_audio_move&&delete_source) {
unlink(filename);
}
@ -450,6 +432,125 @@ void MainObject::RelinkAudio(const QString &srcdir) const
}
void MainObject::RelinkCut(const QString &src_filename,const QString &cutname,
const QString &title,
QString *firstdest,bool *delete_src) const
{
printf(" Recovering %06u/%03d [%s]...",
RDCut::cartNumber(cutname),RDCut::cutNumber(cutname),
title.toUtf8().constData());
fflush(stdout);
if(db_relink_audio_move) {
unlink(RDCut::pathName(cutname));
if(link(src_filename,RDCut::pathName(cutname))<0) {
if(errno==EXDEV) { // We're crossing filesystems, so do a copy
if(firstdest->isEmpty()) {
if(CopyToAudioStore(RDCut::pathName(cutname),src_filename)) {
*firstdest=RDCut::pathName(cutname);
}
else {
fprintf(stderr,"unable to copy file \"%s\"\n",
(const char *)src_filename);
*delete_src=false;
}
}
else {
unlink(RDCut::pathName(cutname));
link(*firstdest,RDCut::pathName(cutname));
}
}
else {
fprintf(stderr,"unable to move file \"%s\" [%s]\n",
(const char *)src_filename,strerror(errno));
*delete_src=false;
}
}
else {
chown(RDCut::pathName(cutname),db_config->uid(),db_config->gid());
chmod(RDCut::pathName(cutname),S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP);
}
}
else {
if(firstdest->isEmpty()) {
if(CopyToAudioStore(RDCut::pathName(cutname),src_filename)) {
*firstdest=RDCut::pathName(cutname);
}
else {
fprintf(stderr,"unable to copy file \"%s\"\n",
(const char *)src_filename);
}
}
else {
unlink(RDCut::pathName(cutname));
link(*firstdest,RDCut::pathName(cutname));
}
}
printf(" done.\n");
}
void MainObject::RelinkCast(const QString &src_filename,const QString &keyname,
unsigned cast_id,const QString &title,
const QString &audio_filename,
QString *firstdest,bool *delete_src) const
{
QString destpath=QString(RD_AUDIO_ROOT)+"/"+audio_filename;
printf(" Recovering RSS item %s:%u [%s]...",
keyname.toUtf8().constData(),cast_id,title.toUtf8().constData());
fflush(stdout);
if(db_relink_audio_move) {
unlink(destpath);
if(link(src_filename,destpath)<0) {
if(errno==EXDEV) { // We're crossing filesystems, so do a copy
if(firstdest->isEmpty()) {
if(CopyToAudioStore(destpath,src_filename)) {
*firstdest=destpath;
}
else {
fprintf(stderr,"unable to copy file \"%s\"\n",
(const char *)src_filename);
*delete_src=false;
}
}
else {
unlink(destpath);
link(*firstdest,destpath);
}
}
else {
fprintf(stderr,"unable to move file \"%s\" [%s]\n",
(const char *)src_filename,strerror(errno));
*delete_src=false;
}
}
else {
chown(destpath,db_config->uid(),db_config->gid());
chmod(destpath,S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP);
}
}
else {
if(firstdest->isEmpty()) {
unlink(destpath);
if(CopyToAudioStore(destpath,src_filename)) {
*firstdest=destpath;
}
else {
fprintf(stderr,"unable to copy file \"%s\" [%s]\n",
(const char *)src_filename,strerror(errno));
}
}
else {
unlink(destpath);
link(*firstdest,destpath);
}
}
printf(" done.\n");
}
void MainObject::CheckOrphanedTracks() const
{
QString sql="select NUMBER,TITLE,OWNER from CART where OWNER!=\"\"";
@ -865,7 +966,8 @@ bool MainObject::UserResponse() const
}
bool MainObject::CopyFile(const QString &destfile,const QString &srcfile) const
bool MainObject::CopyToAudioStore(const QString &destfile,
const QString &srcfile) const
{
int src_fd=-1;
struct stat src_stat;
@ -896,10 +998,10 @@ bool MainObject::CopyFile(const QString &destfile,const QString &srcfile) const
write(dest_fd,data,n);
}
free(data);
fchown(dest_fd,150,150); // FIXME: do name lookup!
fchown(dest_fd,db_config->uid(),db_config->gid());
fchmod(dest_fd,S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP);
close(dest_fd);
close(src_fd);
return true;
}

View File

@ -50,6 +50,13 @@ class MainObject : public QObject
const QString &new_filename,const QString &new_str,
QString *err_msg);
void RelinkAudio(const QString &srcdir) const;
void RelinkCut(const QString &src_filename,const QString &cutname,
const QString &title,
QString *firstdest,bool *delete_src) const;
void RelinkCast(const QString &src_filename,const QString &keyname,
unsigned cast_id,const QString &title,
const QString &audio_filename,
QString *firstdest,bool *delete_src) const;
void CheckOrphanedTracks() const;
void CheckCutCounts() const;
void CheckPendingCarts() const;
@ -62,7 +69,7 @@ class MainObject : public QObject
void RehashCut(const QString &cutnum) const;
void SetCutLength(const QString &cutname,int len) const;
void RemoveCart(unsigned cartnum);
bool CopyFile(const QString &destfile,const QString &srcfile) const;
bool CopyToAudioStore(const QString &destfile,const QString &srcfile) const;
bool UserResponse() const;
//

View File

@ -40,6 +40,16 @@ bool MainObject::RevertSchema(int cur_schema,int set_schema,QString *err_msg)
// NEW SCHEMA REVERSIONS GO HERE...
//
// Revert 335
//
if((cur_schema==335)&&(set_schema<cur_schema)) {
DropIndex("PODCASTS","SHA1_HASH_IDX");
DropColumn("PODCASTS","SHA1_HASH");
WriteSchemaVersion(--cur_schema);
}
//
// Revert 334
//

View File

@ -161,7 +161,7 @@ void MainObject::InitializeSchemaMap() {
global_version_map["3.2"]=311;
global_version_map["3.3"]=314;
global_version_map["3.4"]=317;
global_version_map["4.0"]=334;
global_version_map["4.0"]=335;
}

View File

@ -10243,6 +10243,20 @@ bool MainObject::UpdateSchema(int cur_schema,int set_schema,QString *err_msg)
WriteSchemaVersion(++cur_schema);
}
if((cur_schema<335)&&(set_schema>cur_schema)) {
sql=QString("alter table PODCASTS add column ")+
"SHA1_HASH varchar(40) after AUDIO_TIME";
if(!RDSqlQuery::apply(sql,err_msg)) {
return false;
}
sql=QString("alter table PODCASTS add index SHA1_HASH_IDX(SHA1_HASH)");
if(!RDSqlQuery::apply(sql,err_msg)) {
return false;
}
WriteSchemaVersion(++cur_schema);
}

View File

@ -29,6 +29,7 @@
#include <rdescape_string.h>
#include <rdformpost.h>
#include <rdgroup.h>
#include <rdhash.h>
#include <rdpodcast.h>
#include <rduser.h>
#include <rdweb.h>
@ -83,6 +84,7 @@ void Xport::SavePodcast()
delete cast;
XmlExit(err_msg.toUtf8(),500,"podcasts.cpp",LINE_NUMBER);
}
cast->setSha1Hash(RDSha1Hash(destpath));
printf("Content-type: text/html; charset: UTF-8\n");
printf("Status: 200\n\n");
@ -191,6 +193,7 @@ void Xport::DeletePodcast()
XmlExit(err_msg.toUtf8(),500,"podcasts.cpp",LINE_NUMBER);
}
}
cast->setSha1Hash();
printf("Content-type: text/html; charset: UTF-8\n");
printf("Status: 200\n\n");